import { SCRATCH_HEADERS, SECURITY_HEADERS } from "./static"; export const SCRATCH_BANNER = `
Unclaimed scratch site: anonymous, temporary content. Do not enter passwords or payment details here.
`; const HTTP_SWITCHING = 101; /** * WebSocket upgrades can't be rebuilt: `new Response` rejects status 101 and would drop `webSocket`. They carry no * document, so they pass through untouched. */ export function isUpgrade(response: Response): boolean { return response.status === HTTP_SWITCHING || Boolean((response as { webSocket?: unknown }).webSocket); } function rebuilt(response: Response, headers: Headers, body: BodyInit | null = response.body): Response { return new Response(body, { status: response.status, statusText: response.statusText, headers }); } /** * Every response of an unclaimed scratch site, its `_worker.js` included, carries the scratch security headers * (overriding whatever user code set) and HTML pages get a banner a phishing page can't remove server-side. */ export function hardenScratchResponse(response: Response): Response { if (isUpgrade(response)) return response; const headers = new Headers(response.headers); for (const [name, value] of Object.entries(SCRATCH_HEADERS)) headers.set(name, value); const isHtml = (headers.get("content-type") ?? "").toLowerCase().includes("text/html"); if (!isHtml || !response.body) return rebuilt(response, headers); headers.delete("content-length"); let injected = false; const rewritten = new HTMLRewriter() .on("body", { element(element) { injected = true; element.append(SCRATCH_BANNER, { html: true }); }, }) .onDocument({ end(end) { if (!injected) end.append(SCRATCH_BANNER, { html: true }); }, }) .transform(rebuilt(response, headers)); return rewritten; } /** Claimed sites' workers may tighten or replace a header, but a missing one is put back. */ export function ensureSecurityHeaders(response: Response): Response { if (isUpgrade(response)) return response; const missing = Object.entries(SECURITY_HEADERS).filter(([name]) => !response.headers.has(name)); if (missing.length === 0) return response; const headers = new Headers(response.headers); for (const [name, value] of missing) headers.set(name, value); return rebuilt(response, headers); }