1import {
2 HEADER_ORIGINAL_URL,
3 HEADER_PROXY_SHARED_SECRET,
4} from "../core/globals/ProxyHeaders.shared.ts";
5import type { IncomingMessage } from "node:http";
6import type { TLSSocket } from "node:tls";
7import type { IncomingHttpHeaders } from "node:http";
8
9/**
10 * Resolves the client-facing host of an incoming dev-server request.
11 *
12 * When the dev server runs behind a proxy or tunnel (e.g. ngrok or
13 * `cloudflared`), the `Host` header carries the local address while the
14 * public host arrives in `X-Forwarded-Host`. Preferring the forwarded host
15 * lets the worker see the URL the client actually requested.
16 */
17export function resolveForwardedHost(
18 headers: IncomingHttpHeaders,
19 fallbackHost: string,
20): string {
21 return (
22 firstHeaderValue(headers["x-forwarded-host"]) ??
23 firstHeaderValue(headers.host) ??
24 fallbackHost
25 );
26}
27
28function firstHeaderValue(
29 value: string | Array<string> | undefined,
30): string | undefined {
31 // Proxy chains may append to a single header ("host1, host2") instead of
32 // repeating it; only the first entry is the client-facing host.
33 const raw = Array.isArray(value) ? value[0] : value;
34 const first = raw?.split(",")[0]?.trim();
35 return first ? first : undefined;
36}
37
38/** Sign the client-facing URL for the runtime entry worker, including TLS termination. */
39export function proxyRequestHeaders(
40 request: IncomingMessage,
41 target: URL,
42 proxySharedSecret: string,
43): IncomingHttpHeaders {
44 const original = new URL(target);
45 const protocol = firstHeaderValue(request.headers["x-forwarded-proto"]);
46 original.protocol =
47 protocol === "https" || protocol === "http"
48 ? `${protocol}:`
49 : (request.socket as TLSSocket).encrypted
50 ? "https:"
51 : "http:";
52 original.port = "";
53 original.host = resolveForwardedHost(request.headers, target.host);
54 return {
55 ...request.headers,
56 host: original.host,
57 [HEADER_ORIGINAL_URL.toLowerCase()]: original.href,
58 [HEADER_PROXY_SHARED_SECRET.toLowerCase()]: proxySharedSecret,
59 };
60}