// Run by spa.test.ts in a subprocess with HTTP_PROXY/ALL_PROXY pointing at an unreachable proxy. import { spaWithCsp } from "../../src/server/spa"; /** * The test client speaks raw HTTP/1.1 over Bun.connect: fetch and Bun's node:http both honour HTTP_PROXY, and * only the code under test should meet the proxy. */ async function get(url: string, method = "GET", headers: Record = {}): Promise<{ status: number; headers: Record; body: string }> { const u = new URL(url); let raw = ""; await new Promise((resolve, reject) => { Bun.connect({ hostname: u.hostname, port: Number(u.port), socket: { open(sock) { const body = method === "POST" ? "x" : ""; const lines = [`${method} ${u.pathname}${u.search} HTTP/1.1`, `Host: ${u.host}`, "Connection: close", ...Object.entries(headers).map(([k, v]) => `${k}: ${v}`)]; if (body) lines.push(`Content-Length: ${body.length}`); sock.write(`${lines.join("\r\n")}\r\n\r\n${body}`); }, data(sock, d) { raw += new TextDecoder().decode(d); // Bun keeps the connection open despite "Connection: close"; stop once the whole response is here. const end = raw.indexOf("\r\n\r\n"); if (end < 0) return; const len = /\r\ncontent-length:\s*(\d+)/i.exec(raw.slice(0, end)); const done = len ? raw.length - end - 4 >= Number(len[1]) : /\r\n0\r\n\r\n$/.test(raw); if (done) { sock.end(); resolve(); } }, close() { resolve(); }, error(_s, e) { reject(e); }, }, }).catch(reject); }); const [head, ...rest] = raw.split("\r\n\r\n"); const [statusLine, ...hl] = head!.split("\r\n"); const hs = Object.fromEntries(hl.map((l) => [l.slice(0, l.indexOf(":")).toLowerCase(), l.slice(l.indexOf(":") + 1).trim()])); let body = rest.join("\r\n\r\n"); if (hs["transfer-encoding"] === "chunked") { let out = "", i = 0; for (;;) { const nl = body.indexOf("\r\n", i); const n = parseInt(body.slice(i, nl), 16); if (!n) break; out += body.slice(nl + 2, nl + 2 + n); i = nl + 2 + n + 2; } body = out; } return { status: Number(statusLine!.split(" ")[1]), headers: hs, body }; } // Control, before spaWithCsp touches NO_PROXY: a plain loopback fetch in this process goes to the env proxy. const plain = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("direct") }); const controlViaEnvProxy = await fetch(`http://127.0.0.1:${plain.port}/`, { signal: AbortSignal.timeout(2000) }).then((r) => `status ${r.status}`, (e) => `error ${(e as Error).name}`); plain.stop(true); const seen: Record[] = []; const DOC = '
'; const spa = spaWithCsp((req) => { if (new URL(req.url).pathname === "/app.js") return new Response("console.log(1)", { headers: { "Content-Type": "text/javascript" } }); seen.push({ cookie: req.headers.get("cookie"), authorization: req.headers.get("authorization"), xff: req.headers.get("x-forwarded-for"), accept: req.headers.get("accept") }); return new Response(DOC, { headers: { "Content-Type": "text/html;charset=utf-8" } }); }, { development: false }); const server = Bun.serve({ hostname: "127.0.0.1", port: 0, routes: spa.routes }); const url = `http://127.0.0.1:${server.port}/c/sample`; const out: Record = {}; try { out.controlViaEnvProxy = controlViaEnvProxy; const r = await get(url, "GET", { cookie: "better-auth.session_token=secret", authorization: "Bearer secret", "x-forwarded-for": "1.2.3.4", accept: "text/html" }); out.status = r.status; out.csp = r.headers["content-security-policy"]; out.body = r.body; out.post = (await get(url, "POST")).status; const asset = await get(`http://127.0.0.1:${server.port}/app.js`); out.asset = `${asset.status} ${asset.headers["content-type"]} ${asset.body}`; out.noProxy = process.env.NO_PROXY; out.seen = seen; } finally { server.stop(true); spa.stop(); } console.log(JSON.stringify(out));