import http, { type IncomingMessage } from "node:http"; import https from "node:https"; import { EgressError } from "./errors.js"; import type { ValidatedEndpoint } from "./policy.js"; export interface WireResponse { status: number; statusText: string; headers: Headers; body: Buffer } /** A fresh socket for every hop: no proxy, second DNS lookup, or unvalidated pooled connection. */ export function requestPinned(endpoint: ValidatedEndpoint, method: string, headers: Headers, body: Buffer | undefined, signal: AbortSignal, maxResponseBytes: number, onHeaders: (status: number, headers: Headers) => Promise): Promise { return new Promise((resolve, reject) => { const requestHeaders = Object.fromEntries(headers.entries()); requestHeaders.host = endpoint.url.host; requestHeaders["accept-encoding"] = "identity"; delete requestHeaders["content-length"]; delete requestHeaders["transfer-encoding"]; delete requestHeaders.connection; if (body) requestHeaders["content-length"] = String(body.length); let response: IncomingMessage | undefined; let settled = false; const fail = (error: EgressError) => { if (settled) return; settled = true; reject(error); response?.destroy(); request.destroy(); }; const request = (endpoint.url.protocol === "https:" ? https : http).request(endpoint.url, { method, headers: requestHeaders, agent: false, family: endpoint.family, // Keep URL hostname for TLS verification/SNI and Host, but return only the vetted IP. lookup: (_hostname, options, callback) => { if (options.all) callback(null, [{ address: endpoint.pinnedAddress, family: endpoint.family }]); else callback(null, endpoint.pinnedAddress, endpoint.family); }, }, (incoming) => { response = incoming; const responseHeaders = new Headers(); for (let i = 0; i < incoming.rawHeaders.length; i += 2) responseHeaders.append(incoming.rawHeaders[i]!, incoming.rawHeaders[i + 1]!); incoming.on("error", () => fail(new EgressError("NETWORK_ERROR", "Response stream failed"))); incoming.on("aborted", () => fail(new EgressError("NETWORK_ERROR", "Response stream aborted"))); void (async () => { const status = incoming.statusCode ?? 502; await onHeaders(status, responseHeaders); if (settled) return; const hasBody = method !== "HEAD" && ![204, 205, 304].includes(status); const length = Number(responseHeaders.get("content-length")); if (hasBody && Number.isFinite(length) && length > maxResponseBytes) { fail(new EgressError("RESPONSE_TOO_LARGE", "Response exceeds size limit")); return; } // Identity encoding makes both the size cap and returned byte representation explicit. const encoding = responseHeaders.get("content-encoding"); if (hasBody && encoding && encoding.toLowerCase() !== "identity") { fail(new EgressError("NETWORK_ERROR", "Upstream ignored identity encoding")); return; } const chunks: Buffer[] = []; let size = 0; incoming.on("data", (chunk: Buffer) => { size += chunk.length; if (size > maxResponseBytes) fail(new EgressError("RESPONSE_TOO_LARGE", "Response exceeds size limit")); else chunks.push(chunk); }); incoming.on("end", () => { if (settled) return; settled = true; resolve({ status, statusText: incoming.statusMessage ?? "", headers: responseHeaders, body: Buffer.concat(chunks) }); }); })().catch((error: unknown) => fail(error instanceof EgressError ? error : new EgressError("NETWORK_ERROR", "Response processing failed"))); }); const aborted = () => fail(signal.reason instanceof EgressError ? signal.reason : new EgressError("ABORTED", "Request aborted")); signal.addEventListener("abort", aborted, { once: true }); request.on("close", () => signal.removeEventListener("abort", aborted)); request.on("error", () => fail(new EgressError("NETWORK_ERROR", "Connection failed"))); if (signal.aborted) { aborted(); return; } request.end(body); }); }