Public: anyone can read this; the crew can change it.

(0) Trust: test the parent-answer byte caps at register and token, and the parent-made account's SSH sign-in end to end #2326

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2289-0-trust-test-the-parent-answer-byte-caps → main 1 commit fork

  • Merged. scout/2289-0-trust-test-the-parent-answer-byte-caps was merged into main as fc7add8.

Review

Sign in to review this change.
1 file changed +108 −0
test/forge-signin.test.ts +108
@@ -11,6 +11,8 @@ import { join } from "node:path";
1111 import { createApp, type App } from "../src/app.ts";
1212 import { createSession } from "../src/core/sessions.ts";
1313 import { createUser, getUserByName, verifyPassword } from "../src/core/users.ts";
14 import { keepClient, CALLBACK_PATH } from "../src/core/federation.ts";
15 import { forgeSshSession } from "../src/tui/ssh.ts";
1416 import { addKey, findKeyByFingerprint, fingerprint, parseAuthorizedKey } from "../src/core/keys.ts";
1517 import { KeySet } from "../src/http/jwks.ts";
1618 import { serve, type Servers } from "../src/http/server.ts";
@@ -242,6 +244,43 @@ describe("sign in through the parent forge", () => {
242244 if (!parsed.ok) throw new Error("key");
243245 expect(addKey(child.db, local.id, "laptop", parsed.value).ok).toBe(true);
244246 expect(findKeyByFingerprint(child.db, fingerprint(parsed.value.blob))?.userId).toBe(local.id);
247
248 // And the terminal itself: a shell opened with that key is that account's.
249 let screen = "";
250 const closers: (() => void)[] = [];
251 const session = {
252 key: { fingerprint: fingerprint(parsed.value.blob), line },
253 username: "whoever",
254 remoteAddress: "203.0.113.9",
255 kind: "shell" as const,
256 command: undefined,
257 term: "xterm-256color",
258 env: {},
259 cols: 100,
260 rows: 30,
261 widthPx: 0,
262 heightPx: 0,
263 ended: false,
264 pending: 0,
265 write(data: Uint8Array | string) {
266 screen += typeof data === "string" ? data : Buffer.from(data).toString();
267 },
268 writeStderr() {},
269 onData() {},
270 onResize() {},
271 onEof() {},
272 onClose(cb: () => void) {
273 closers.push(cb);
274 },
275 exit() {
276 for (const cb of closers) cb();
277 },
278 drained: async () => {},
279 };
280 forgeSshSession(child)(session as never);
281 await Bun.sleep(300);
282 session.exit();
283 expect(screen).toContain("alice");
245284 });
246285
247286 test("starting a sign-in is throttled, so a flood cannot push out those in flight", async () => {
@@ -387,3 +426,72 @@ describe("a parent's oversized answer", () => {
387426 expect(pulled.n).toBeLessThan(200);
388427 });
389428 });
429
430 describe("a parent's oversized register and token answers", () => {
431 const realFetch = globalThis.fetch;
432 const PARENT = "http://parent.invalid";
433 let dir = "";
434 let child: App;
435 let servers: Servers;
436 let pulled = { n: 0 };
437 let streaming: "register" | "token" | "" = "";
438
439 /** A body that never ends, counting how many chunks were pulled from it. */
440 const endless = (): Response => {
441 pulled = { n: 0 };
442 return new Response(
443 new ReadableStream<Uint8Array>({
444 pull(controller) {
445 pulled.n++;
446 controller.enqueue(new Uint8Array(1024).fill(32));
447 },
448 }),
449 { status: 201, headers: { "content-type": "application/json" } },
450 );
451 };
452
453 beforeAll(async () => {
454 dir = await mkdtemp(join(tmpdir(), "forge-signin-"));
455 // The parent is this stub; every other address is reached as usual.
456 globalThis.fetch = ((input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
457 const url = String(input instanceof Request ? input.url : input);
458 if (url === `${PARENT}/oauth/register` && streaming === "register") return Promise.resolve(endless());
459 if (url === `${PARENT}/oauth/token` && streaming === "token") return Promise.resolve(endless());
460 return realFetch(input, init);
461 }) as typeof fetch;
462 child = createApp({ dataDir: dir, port: 0, parentUrl: PARENT, parentLoopback: true });
463 servers = await serve(child);
464 });
465 afterAll(async () => {
466 globalThis.fetch = realFetch;
467 servers.stop();
468 await rm(dir, { recursive: true, force: true });
469 });
470
471 test("a registration that streams forever is a 502, and few chunks are pulled", async () => {
472 streaming = "register";
473 const res = await fetch(`${servers.url}/signin/parent`, { redirect: "manual" });
474 expect(res.status).toBe(502);
475 expect(pulled.n).toBeGreaterThan(0);
476 expect(pulled.n).toBeLessThan(100);
477 expect(child.db.query("SELECT COUNT(*) AS n FROM rp_client").get()).toEqual({ n: 0 });
478 });
479
480 test("a token answer that streams forever is a 502 with no session, and few chunks are pulled", async () => {
481 streaming = "";
482 keepClient(child.db, `${PARENT}`, `${servers.url}${CALLBACK_PATH}`, "client-1");
483 const start = await fetch(`${servers.url}/signin/parent`, { redirect: "manual" });
484 expect(start.status).toBe(303);
485 const cookie = start.headers.getSetCookie()[0]!.split(";")[0]!;
486 const state = cookie.slice("forge_rp=".length);
487 streaming = "token";
488 const res = await fetch(`${servers.url}${CALLBACK_PATH}?code=abc&state=${state}`, {
489 redirect: "manual",
490 headers: { cookie },
491 });
492 expect(res.status).toBe(502);
493 expect(res.headers.getSetCookie().some((c) => c.startsWith("forge_session=") && !c.includes("Max-Age=0"))).toBe(false);
494 expect(pulled.n).toBeGreaterThan(0);
495 expect(pulled.n).toBeLessThan(100);
496 });
497 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs