Ctrl K
ComunidadGuías…

proxy.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { NextResponse, type NextRequest } from "next/server";

/**
 * A plain `host[:port]`. The Host header is the caller's to choose, and one
 * carrying a `;` would end the directive it is written into and start another.
 */
const HOST_PATTERN = /^[a-z0-9.-]+(?::\d+)?$/i;

type PolicyInput = {
  nonce: string;
  /** The host the page was requested on, which is where its room socket lives. */
  host: string;
  /** Served over https, as every deployment is; plain http is a local run. */
  secure: boolean;
  dev: boolean;
};

/**
 * The page's Content-Security-Policy.
 *
 * The room key lives in the URL fragment, so a script that should not be on
 * the page could read it and undo the end-to-end encryption. Scripts are
 * therefore allowed by a per-request nonce and nothing else: no inline script
 * without it, and no host allowlist for one to be loaded from.
 *
 * Styles are looser. Sonner injects a `<style>` that carries no nonce, and
 * several components render `style` attributes, so inline styles stay allowed.
 *
 * WebRTC and its STUN/TURN servers are not governed by any of this.
 */
function contentSecurityPolicy({ nonce, host, secure, dev }: PolicyInput): string {
  const directives = [
    "default-src 'self'",
    // React evaluates strings in development to rebuild server error stacks.
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${dev ? " 'unsafe-eval'" : ""}`,
    "style-src 'self' 'unsafe-inline'",
    // Thumbnails of received images are object URLs.
    "img-src 'self' blob: data:",
    "media-src 'self' blob:",
    // Named as well as 'self': older Safari does not take 'self' to cover the
    // WebSocket scheme of the same host.
    `connect-src 'self'${HOST_PATTERN.test(host) ? ` ${secure ? "wss" : "ws"}://${host}` : ""}`,
    // Stated, where the manifest and fonts are left to default-src: a worker
    // falls back to script-src first, and 'strict-dynamic' there drops 'self'.
    "worker-src 'self'",
    "object-src 'none'",
    "base-uri 'self'",
    "form-action 'self'",
    "frame-ancestors 'none'",
  ];
  if (secure) {
    directives.push("upgrade-insecure-requests");
  }
  return directives.join("; ");
}

export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const policy = contentSecurityPolicy({
    nonce,
    // The header, not `nextUrl.host`: outside Vercel that is the address the
    // server is bound to, which is not where the browser opens the socket.
    host: request.headers.get("host") ?? "",
    secure: request.nextUrl.protocol === "https:",
    dev: process.env.NODE_ENV === "development",
  });

  // On the request as well as the response: Next reads the nonce out of the
  // request's policy while rendering and applies it to its own scripts.
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce);
  requestHeaders.set("Content-Security-Policy", policy);

  const response = NextResponse.next({ request: { headers: requestHeaders } });
  response.headers.set("Content-Security-Policy", policy);
  return response;
}

export const config = {
  matcher: [
    {
      // Pages only. The room API under /rooms (the socket upgrade included),
      // the share target, build assets and anything with a file extension
      // render no document for a policy to protect. `/room/…` is a page, so
      // only `rooms` as a whole segment is skipped.
      source:
        "/((?!rooms(?:/|$)|share-target|_next/static|_next/image|_vercel|.*\\.\\w+$).*)",
      missing: [
        { type: "header", key: "next-router-prefetch" },
        { type: "header", key: "purpose", value: "prefetch" },
      ],
    },
  ],
};