56 lines · 2.5 KB
import { SCRATCH_HEADERS, SECURITY_HEADERS } from "./static";

export const SCRATCH_BANNER = `<div role="note" data-gitfor-scratch style="all:initial;position:fixed;z-index:2147483647;left:0;right:0;bottom:0;padding:6px 12px;background:#7c2d12;color:#fff;font:13px/1.4 system-ui,sans-serif;text-align:center">Unclaimed scratch site: anonymous, temporary content. Do not enter passwords or payment details here.</div>`;

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 });