Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
77 lines
4.1 KiB
TypeScript
77 lines
4.1 KiB
TypeScript
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<void>): Promise<WireResponse> {
|
|
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);
|
|
});
|
|
}
|