Tactical Media Broadcast Hub — initial release
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/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
77
packages/egress/src/transport.ts
Normal file
77
packages/egress/src/transport.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
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);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue