Files
ditto.site/compiler/.bridge/bridge.mjs
T
2026-06-29 15:11:48 -07:00

100 lines
4.6 KiB
JavaScript

// Local capture bridge: Chromium → (this HTTP proxy, TLS-terminated locally) → Node
// CONNECT-tunnel through the agent proxy → origin. Exists because Chromium 141's TLS
// ClientHello is closed by the egress proxy during the handshake, while Node/curl/openssl
// ClientHellos are accepted. Chromium talks plaintext-after-TLS to us (we present a static
// self-signed cert; capture runs with ignoreHTTPSErrors so host/CA mismatch is fine), and
// WE re-originate every request over a Node TLS socket the proxy is happy with.
import net from "node:net";
import http from "node:http";
import tls from "node:tls";
import fs from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const DIR = dirname(fileURLToPath(import.meta.url));
const KEY = fs.readFileSync(join(DIR, "key.pem"));
const CERT = fs.readFileSync(join(DIR, "cert.pem"));
const CA = fs.readFileSync("/root/.ccr/ca-bundle.crt");
const LISTEN_PORT = Number(process.env.BRIDGE_PORT || 8899);
// The real egress proxy we tunnel through (the one Chromium can't TLS-handshake with).
const upstream = new URL(process.env.UPSTREAM_PROXY || "http://127.0.0.1:37671");
const UP_HOST = upstream.hostname;
const UP_PORT = Number(upstream.port || 80);
/** Open a socket to `host:port` by CONNECT-tunnelling through the agent proxy, then (for
* 443) upgrading to TLS that the proxy accepts. Calls back with the ready socket. */
function tunnel(host, port, cb) {
const raw = net.connect(UP_PORT, UP_HOST);
let settled = false;
const fail = (e) => { if (!settled) { settled = true; cb(e); } raw.destroy(); };
raw.once("error", fail);
raw.on("connect", () => {
raw.write(`CONNECT ${host}:${port} HTTP/1.1\r\nHost: ${host}:${port}\r\n\r\n`);
});
let buf = Buffer.alloc(0);
const onData = (d) => {
buf = Buffer.concat([buf, d]);
const i = buf.indexOf("\r\n\r\n");
if (i < 0) return;
raw.removeListener("data", onData);
const status = buf.slice(0, buf.indexOf("\r\n")).toString();
if (!/ 200 /.test(status)) return fail(new Error(`upstream CONNECT ${host}:${port}${status}`));
raw.removeListener("error", fail);
if (port === 443) {
const t = tls.connect({ socket: raw, servername: host, ca: CA, ALPNProtocols: ["http/1.1"] }, () => {
if (!settled) { settled = true; cb(null, t); }
});
t.once("error", (e) => { if (!settled) { settled = true; cb(e); } });
} else {
if (!settled) { settled = true; cb(null, raw); }
}
};
raw.on("data", onData);
}
// An internal HTTP server that PARSES the plaintext requests Chromium sends us (after we
// TLS-terminate its tunnel) and re-issues them to the origin over a fresh upstream tunnel.
const origin = http.createServer((creq, cres) => {
const host = creq.socket.__host;
const opts = {
method: creq.method,
path: creq.url,
headers: { ...creq.headers, host: creq.headers.host || host, connection: "close" },
createConnection: (_o, cb) => tunnel(host, 443, cb),
};
const preq = http.request(opts, (pres) => {
cres.writeHead(pres.statusCode || 502, pres.headers);
pres.pipe(cres);
});
preq.once("error", (e) => { if (!cres.headersSent) cres.writeHead(502); cres.end("bridge upstream error: " + e.message); });
creq.pipe(preq);
});
origin.on("clientError", (_e, sock) => sock.destroy());
const proxy = http.createServer((req, res) => {
// Plain-HTTP proxied request (rare for these sites): forward host:80 through the tunnel.
const u = new URL(req.url);
const opts = { method: req.method, path: u.pathname + u.search, headers: { ...req.headers, connection: "close" },
createConnection: (_o, cb) => tunnel(u.hostname, Number(u.port || 80), cb) };
const preq = http.request(opts, (pres) => { res.writeHead(pres.statusCode || 502, pres.headers); pres.pipe(res); });
preq.once("error", (e) => { if (!res.headersSent) res.writeHead(502); res.end("bridge error: " + e.message); });
req.pipe(preq);
});
proxy.on("connect", (req, clientSocket) => {
const [host] = req.url.split(":");
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
// We are the TLS *server* for Chromium's tunnel; present our static cert.
const tlsSock = new tls.TLSSocket(clientSocket, { isServer: true, key: KEY, cert: CERT });
tlsSock.__host = host;
tlsSock.on("error", () => tlsSock.destroy());
// Hand the decrypted byte stream to the internal HTTP parser.
origin.emit("connection", tlsSock);
});
proxy.on("clientError", (_e, sock) => sock.destroy());
proxy.listen(LISTEN_PORT, "127.0.0.1", () => {
console.log(`bridge listening on http://127.0.0.1:${LISTEN_PORT} → upstream ${UP_HOST}:${UP_PORT}`);
});