mirror of
https://github.com/tiennm99/rplace.git
synced 2026-10-15 00:18:00 +00:00
feat(canvas): cookie+IP rate-limit identity and broadcast sequence numbers
- resolveIdentity prefers an opaque rplace_id cookie; falls back to a
cf-connecting-ip hash; in production a request with neither now returns
500 no_identity instead of bucketing all such traffic together
- /api/canvas issues Set-Cookie when no cookie is present so subsequent
requests escape NAT-shared IP buckets (mobile/CGNAT users)
- DO maintains an in-memory monotonic broadcast counter; broadcast frames
carry { seq } so the client can detect missed pixels and refetch
- client tracks lastSeq, refetches on gap, resets on every (re)connect
NAT/CGNAT users previously shared a single 1Hz bucket per egress IP. With
cookie identity they each get their own bucket. Cookie is HttpOnly, Secure,
SameSite=Lax, 1y Max-Age. Stripped/cleared cookies fall through to IP.
The seq counter resets on DO hibernation rehydrate; client always refetches
on reconnect, so a reset is indistinguishable from a fresh connect.
Plan: plans/260510-0232-fix-do-migration-followups/phase-02-cookie-ip-identity.md
This commit is contained in:
1 parent
3c1263add6
commit
42d1ca19ee
7 files changed
+283
-42
No files matched your search
+42
-5
@@ -1,5 +1,6 @@
|
||||
import { Hono } from 'hono';
|
||||
import { getUserId } from './lib/get-user-id.js';
|
||||
import { resolveIdentity, NoIdentityError } from './lib/get-user-id.js';
|
||||
import { formatSetCookie } from './lib/cookie.js';
|
||||
import { CANVAS_WIDTH, CANVAS_HEIGHT, MAX_COLORS, MAX_BATCH_SIZE } from './lib/constants.js';
|
||||
|
||||
export { CanvasRoom } from './durable-objects/canvas-room.js';
|
||||
@@ -9,14 +10,42 @@ const app = new Hono();
|
||||
// ~64 bytes is generous per pixel JSON object {"x":2047,"y":2047,"color":31}
|
||||
const MAX_BODY_BYTES = MAX_BATCH_SIZE * 64;
|
||||
|
||||
const COOKIE_OPTS = {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'Lax',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 year
|
||||
};
|
||||
|
||||
/** Resolve the singleton CanvasRoom DO stub. */
|
||||
function room(env) {
|
||||
return env.CANVAS_ROOM.get(env.CANVAS_ROOM.idFromName('main'));
|
||||
}
|
||||
|
||||
/** GET /api/canvas — full canvas binary, served by the DO directly. */
|
||||
/** GET /api/canvas — full canvas binary, served by the DO directly. Issues
|
||||
* the rplace_id cookie on first request so future calls bypass NAT-shared
|
||||
* IP buckets. */
|
||||
app.get('/api/canvas', async (c) => {
|
||||
return room(c.env).fetch('http://do/canvas');
|
||||
let identity;
|
||||
try {
|
||||
identity = await resolveIdentity(c.req.raw, c.env);
|
||||
} catch (err) {
|
||||
if (err instanceof NoIdentityError) {
|
||||
return c.json({ error: 'no_identity' }, 500);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
const upstream = await room(c.env).fetch('http://do/canvas');
|
||||
if (!identity.mintCookieValue) return upstream;
|
||||
// Attach Set-Cookie without mutating the upstream Response (its body is a
|
||||
// stream; new Response keeps it linked).
|
||||
const out = new Response(upstream.body, upstream);
|
||||
out.headers.append(
|
||||
'Set-Cookie',
|
||||
formatSetCookie('rplace_id', identity.mintCookieValue, COOKIE_OPTS),
|
||||
);
|
||||
return out;
|
||||
});
|
||||
|
||||
/** POST /api/place — validate at the edge, forward to the DO. */
|
||||
@@ -57,13 +86,21 @@ app.post('/api/place', async (c) => {
|
||||
}
|
||||
}
|
||||
|
||||
const userId = await getUserId(c.req.raw);
|
||||
let identity;
|
||||
try {
|
||||
identity = await resolveIdentity(c.req.raw, c.env);
|
||||
} catch (err) {
|
||||
if (err instanceof NoIdentityError) {
|
||||
return c.json({ error: 'no_identity' }, 500);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
// DO does cooldown check + pixel write + broadcast in one atomic step.
|
||||
return room(c.env).fetch('http://do/place', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId, pixels }),
|
||||
body: JSON.stringify({ userId: identity.id, pixels }),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user