diff --git a/main.js b/main.js index d3f9c70..4b47fcb 100644 --- a/main.js +++ b/main.js @@ -8,6 +8,7 @@ const { app, BrowserWindow, WebContentsView, ipcMain, protocol, session, Menu, c const path = require("path"); const http = require("http"); const https = require("https"); +const tls = require("tls"); const { spawn } = require("child_process"); const fs = require("fs"); const WebSocket = require("ws"); @@ -535,6 +536,70 @@ async function contentFetch(url, init = {}) { const r = await fetch(url, init); return { status: r.status, contentType: r.headers.get("content-type"), buffer: Buffer.from(await r.arrayBuffer()) }; } + +// BNS `ip`-record fetch. The default `fetch` fails here for two reasons: +// 1. It follows the site's HTTP→HTTPS 301 into `https://./`, which +// isn't in ICANN DNS → "fetch failed". +// 2. It validates TLS against the public CA store, but BNS certs are signed +// by per-machine BNS root CAs — the trust anchor is the on-chain `tls` +// record's SHA-256 fingerprint, which we pin against here. See +// Argus/src/lib/ca.js for the underlying trust model, and the parallel +// implementation in Argus/src/gateway/public-gateway.mjs — keep both in +// step. This code path also runs through Tor when Tor is on. +function certFp(cert) { + const fp = cert && cert.fingerprint256; + return fp ? fp.toLowerCase().replace(/:/g, "") : ""; +} +async function pinnedHttpsGet(ip, port, servername, reqPath, expectedFp) { + const useTor = torState === "on"; + if (useTor) await loadSocks(); + return new Promise((resolve, reject) => { + const opts = { + host: ip, port, servername, method: "GET", path: reqPath, + headers: { host: servername, "user-agent": "theseus/1" }, + }; + opts["rejectUnauthorized"] = false; // fingerprint pin below is the gate. + if (useTor) opts.agent = new SocksProxyAgent(`socks5h://127.0.0.1:${TOR_PORT}`); + const req = https.request(opts, (res) => { + const gotFp = certFp(res.socket.getPeerCertificate(false)); + if (gotFp !== expectedFp) { + res.socket.destroy(); + return reject(new Error(`tls fingerprint mismatch for ${servername}: got ${gotFp}, expected ${expectedFp}`)); + } + const chunks = []; + res.on("data", (c) => chunks.push(c)); + res.on("end", () => resolve({ status: res.statusCode, contentType: res.headers["content-type"], buffer: Buffer.concat(chunks) })); + }); + req.setTimeout(15000, () => { req.destroy(new Error("tls request timeout")); }); + req.on("error", reject); + req.end(); + }); +} +async function httpGetByIp(ip, reqPath, hostHeader) { + const useTor = torState === "on"; + if (useTor) await loadSocks(); + return new Promise((resolve, reject) => { + const opts = { + host: ip, port: 80, method: "GET", path: reqPath, + headers: { host: hostHeader, "user-agent": "theseus/1" }, + }; + if (useTor) opts.agent = new SocksProxyAgent(`socks5h://127.0.0.1:${TOR_PORT}`); + const req = http.request(opts, (res) => { + const chunks = []; + res.on("data", (c) => chunks.push(c)); + res.on("end", () => resolve({ status: res.statusCode, contentType: res.headers["content-type"], buffer: Buffer.concat(chunks) })); + }); + req.on("error", reject); req.end(); + }); +} +// Compose: pinned HTTPS if the on-chain `tls` fingerprint is available, else +// plain HTTP by IP. No silent HTTP fallback on pin failure — a mismatch means +// "not the site the chain says it is" and returning HTTP anyway would defeat +// the pin. +async function ipRequest(ip, reqPath, hostHeader, tlsFingerprint) { + if (tlsFingerprint) return await pinnedHttpsGet(ip, 443, hostHeader, reqPath, String(tlsFingerprint).toLowerCase()); + return await httpGetByIp(ip, reqPath, hostHeader); +} // OpenSearch "scan": fetch a page's OpenSearch description and turn its HTML // search template into our { name, url-with-%s } form. async function fetchOpenSearch(href) { @@ -642,7 +707,11 @@ async function serveBns(request) { // for the full argument; keep this in step with that file. const isSubdomain = host !== rec.entry.name; const serveIp = async () => { - const up = await contentFetch(`http://${r.ip}${reqPath}${url.search}`, { headers: { host } }); + // See ipRequest above: HTTPS-with-fingerprint-pin against the on-chain `tls` + // record when available, HTTP fallback when not. Fixes serving BNS names + // whose server redirects :80→:443 (the plain-fetch path chokes on the + // redirect target because it isn't in ICANN DNS). + const up = await ipRequest(r.ip, reqPath + url.search, host, r.tls); return new Response(up.buffer, { status: up.status, headers: { "content-type": up.contentType || guessType(reqPath) } }); }; try {