openvibers/openvibe.space Commits

A space named with a reserved word gets an address; the Spaces API has a rate limit (#199)

OP OpenVibers committed 10 days ago · addccedeb3 · 3 files, +22 −3

'OpenVibe Poll' made openvibe-poll-xxxx eight times, each reserved, and creating the space failed with a 503: the
reserved 'openvibe' part is dropped once (poll-xxxx), or the address falls back to space-xxxx. /api/v1/apps was
mounted before /api/v1's rate limiter with none of its own; it gets 600 a minute (a space's page calls it).

Changes

ddiff --git a/server/hosting.js b/server/hosting.js
iindex a74d508..796cc54 100644
---- a/server/hosting.js
++++ b/server/hosting.js
@@@ -127,7 +127,9 @@ function createHosting(app, { db, auth, config: suppliedConfig, now, resolver, f
if (forumRedirect) app.use(forumRedirect);
app.use(contracts.http.middleware());
app.use('/api/v1/resources', rateLimit({ windowMs: 60_000, max: 240, standardHeaders: true, legacyHeaders: false }), viewers.middleware('api'), require('./registry/resource-control').router({ guard: guard('space.resource.control') }), resourceIndex.router({ guard: guard('space.resource.read') }));
-- app.use('/api/v1/apps', require('./spaces/api').createSpacesApi({ config, spaces: ctx.spaces, data: ctx.spaceData, sites, takedowns, log }));
++ // The Spaces API had no rate limit of its own (it is mounted before /api/v1's): a space's page calls it, so its
++ // limit is wider than the API's (REVIEW-0 11-03).
++ app.use('/api/v1/apps', rateLimit({ windowMs: 60_000, max: 600, standardHeaders: true, legacyHeaders: false }), require('./spaces/api').createSpacesApi({ config, spaces: ctx.spaces, data: ctx.spaceData, sites, takedowns, log }));
app.use('/api/v1', rateLimit({ windowMs: 60_000, max: 240, standardHeaders: true, legacyHeaders: false }), createApi(ctx));
app.use('/preview', rateLimit({ windowMs: 60_000, max: 300, standardHeaders: true, legacyHeaders: false }), viewers.middleware('dashboard'), (req, res, next) => Promise.resolve(tenant.preview(req, res)).catch(next));
{
ddiff --git a/server/spaces/spaces.js b/server/spaces/spaces.js
iindex 866ba11..a48badc 100644
---- a/server/spaces/spaces.js
++++ b/server/spaces/spaces.js
@@@ -146,11 +146,17 @@ function createSpaces({ store, config, projects, sites, deploys, takedowns, outb
}
async function uniqueSlug(name) {
-- const base = slugBase(name);
++ let base = slugBase(name);
for (let i = 0; i < 8; i++) {
const tail = crypto.randomBytes(3).toString('hex').slice(0, 4);
const slug = `${base.slice(0, 34).replace(/-+$/, '')}-${tail}`;
-- try { sites.checkName(slug); } catch { continue; }
++ try { sites.checkName(slug); } catch {
++ // A reserved base stays reserved whatever the tail ("OpenVibe Poll" failed all eight tries with a 503,
++ // REVIEW-0 11-03): drop the reserved "openvibe" part once, or fall back to a plain "space-…".
++ const rest = base.replace(/^openvibe-*/, '');
++ base = rest !== base && rest.length >= 3 ? rest : 'space';
++ continue;
++ }
if (!(await q.slugUsed.get(slug, slug))) return slug;
}
throw new ApiError(503, 'space.slug_unavailable', 'could not find a free address for the space; try another name');
ddiff --git a/test/spaces.test.js b/test/spaces.test.js
iindex a01764b..ef100ed 100644
---- a/test/spaces.test.js
++++ b/test/spaces.test.js
@@@ -68,6 +68,17 @@ function fakeAi() {
assert.match(pub.text, /<link rel="canonical" href="https:\/\/openvibe\.space\/spaces">/);
});
++ await check('a space named with a reserved word still gets an address (REVIEW-0: "OpenVibe Poll" was a 503)', async () => {
++ const viewer = { kind: 'user', subject: carol.subject, user: { username: carol.username } };
++ const index = [{ path: 'index.html', content: '<!doctype html><title>x</title>' }];
++ const poll = await t.ctx.spaces.create(viewer, { name: 'OpenVibe Poll', files: index });
++ assert.match(poll.slug, /^poll-[0-9a-f]{4}$/, poll.slug);
++ const bare = await t.ctx.spaces.create(viewer, { name: 'OpenVibe', files: index });
++ assert.match(bare.slug, /^space-[0-9a-f]{4}$/, bare.slug);
++ await t.ctx.spaces.remove(viewer, poll);
++ await t.ctx.spaces.remove(viewer, bare);
++ });
++
await check('a person starts a space from a template and lands in its editor', async () => {
const r = await form(alice, '/spaces/new', { template: 'guestbook' });
assert.strictEqual(r.status, 303, r.text);