| 1 | import { |
| 2 | HEADER_ORIGINAL_URL, |
| 3 | HEADER_PROXY_SHARED_SECRET, |
| 4 | } from "../core/globals/ProxyHeaders.shared.ts"; |
| 5 | import type { IncomingMessage } from "node:http"; |
| 6 | import type { TLSSocket } from "node:tls"; |
| 7 | import 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 | */ |
| 17 | export 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 | |
| 28 | function 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. */ |
| 39 | export 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 | } |