Theseus: same-host bns subresource routing
Pages served from bns://<name>/… that fetch http(s)://<name>/… over XHR / fetch / <img> / <script> are talking to one endpoint by every reading except Chromium's scheme-strict one — the on-chain IP record, the pinned TLS cert and the cookie jar for bns://<name> all describe one server. Previously the subresource went out via clearnet DNS and either hit hostile Wi-Fi (DNS hijack memory) or came back with a CORS error, breaking socket.io on bns://game.x/checkers/play/. Rewrite rules in session.webRequest.onBeforeRequest: - Must be a SUBRESOURCE (resourceType !== mainFrame). Top-level nav stays on the clearnet scheme the author linked to. - resourceType !== webSocket. bns:// is a fetch-only privileged scheme and Chromium refuses to redirect a ws request to a non-ws scheme. Modern socket.io falls back to XHR polling, which this does route. - Initiator origin is bns://<samehost> where samehost matches the request's target host (case-insensitive, port stripped). Settings toggle "Same-host subresource routing" under Registries lets users turn it off; default is on. Add-on request filters still run AFTER the rewrite (the rewritten bns:// URL does not re-enter the http/https filter path, which is the right default — bns:// content is already protocol-filtered by the serveBns handler).
This commit is contained in:
parent
69390f20fb
commit
f54f711463
2 changed files with 59 additions and 0 deletions
53
main.js
53
main.js
|
|
@ -738,6 +738,13 @@ const SETTINGS_DEFAULTS = {
|
|||
// work regardless of this; the menu entry is just a discoverability
|
||||
// escape hatch for users who hit the F12-vs-airplane-mode foot-gun.
|
||||
contextMenuDevTools: true,
|
||||
// Route http(s)://<samehost> SUBRESOURCE requests from a bns://<samehost>
|
||||
// page back through the bns:// handler, so cookies, pinned TLS and the
|
||||
// on-chain IP record all apply — matching what the author expected when
|
||||
// they wrote `fetch("/api/...")` next to `fetch("https://game.x/api/...")`
|
||||
// on the same page. See the installRequestFilter listener below. Top-
|
||||
// level navigation and non-matching hosts are untouched.
|
||||
bnsSameHostSubresource: true,
|
||||
};
|
||||
// Applying the theme via nativeTheme.themeSource makes prefers-color-scheme update
|
||||
// in every renderer (chrome, settings, popover, page views) with no per-view IPC.
|
||||
|
|
@ -3205,8 +3212,54 @@ function notifyTabChange() {
|
|||
const t = activeTab(); const info = t ? { id: t.id, url: t.url || "" } : null;
|
||||
for (const cb of tabChangeListeners) { try { cb(info); } catch (e) { console.warn("[addons] tab listener failed:", e?.message); } }
|
||||
}
|
||||
// Cross-scheme same-host subresource rewrite: a page served at
|
||||
// bns://<name>/… that fetches https://<name>/… (or http://<name>/…) over
|
||||
// XHR / fetch / <img> / <script> is talking to its own origin by every
|
||||
// reading except Chromium's scheme-strict one. The on-chain IP record,
|
||||
// the pinned TLS cert, and the cookie jar for bns://<name> all describe
|
||||
// one endpoint; sub-resource requests should land on it, not on whatever
|
||||
// clearnet DNS surfaces for the same name (which may be hostile Wi-Fi
|
||||
// hijack, a stale CDN or nothing at all — see memory
|
||||
// [[dns-hijack-aaaa-mangle]]).
|
||||
//
|
||||
// Rewrite rules:
|
||||
// - Must be a SUBRESOURCE (not a mainFrame navigation — a page's own
|
||||
// navigation to https://<name>/other-page is the user's choice and
|
||||
// stays on the clearnet scheme the author linked to).
|
||||
// - Initiator origin must be bns://<samehost> where samehost matches
|
||||
// the request's target host, case-insensitively.
|
||||
// - Scheme is http or https.
|
||||
//
|
||||
// WebSocket upgrades (wss://<samehost>) are NOT rewritten — bns:// is a
|
||||
// fetch-only privileged scheme and Chromium cannot redirect a ws request
|
||||
// to a non-ws scheme. Modern socket.io clients auto-fall-back to XHR
|
||||
// polling, which this rewrite does route through bns://. A dedicated
|
||||
// WebSocket tunnel sits in a follow-up.
|
||||
function bnsSameHostRedirect(details) {
|
||||
if (settings.bnsSameHostSubresource === false) return null;
|
||||
const rt = details.resourceType;
|
||||
if (!rt || rt === "mainFrame" || rt === "webSocket") return null;
|
||||
const url = String(details.url || "");
|
||||
const m = /^(https?:)\/\/([^/]+)(\/[^?#]*)?(\?[^#]*)?(#.*)?$/i.exec(url);
|
||||
if (!m) return null;
|
||||
const scheme = m[1].toLowerCase();
|
||||
if (scheme !== "https:" && scheme !== "http:") return null;
|
||||
const targetHost = m[2].toLowerCase().replace(/:\d+$/, "");
|
||||
const path = m[3] || "/", search = m[4] || "", hash = m[5] || "";
|
||||
const init = String(details.initiatorOrigin || "").toLowerCase();
|
||||
const im = /^bns:\/\/([^/:]+)/.exec(init);
|
||||
if (!im) return null;
|
||||
const initHost = im[1];
|
||||
if (initHost !== targetHost) return null;
|
||||
// A port suffix on the target (https://game.x:8443) is dropped; bns://
|
||||
// addresses the on-chain endpoint for the name, which does not carry
|
||||
// the author's clearnet port. Rewriting here keeps the author's path.
|
||||
return `bns://${targetHost}${path}${search}${hash}`;
|
||||
}
|
||||
function installRequestFilter() {
|
||||
session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
|
||||
const redirect = bnsSameHostRedirect(details);
|
||||
if (redirect) return callback({ redirectURL: redirect });
|
||||
if (!requestFilters.size || !/^https?:/i.test(details.url)) return callback({});
|
||||
let frameUrl = ""; try { frameUrl = (details.frame && details.frame.url) || details.referrer || ""; } catch { frameUrl = details.referrer || ""; }
|
||||
const view = { url: details.url, resourceType: details.resourceType, method: details.method, frameUrl, initiator: details.initiatorOrigin || "", webContentsId: details.webContentsId ?? null };
|
||||
|
|
|
|||
|
|
@ -395,6 +395,7 @@
|
|||
<label class="polrow"><input type="radio" name="collisionPolicy" value="icann-first"><span><b>ICANN first</b> <span class="pmuted">— ICANN wins conflicts; BCDN fills gaps for non-ICANN TLDs.</span></span></label>
|
||||
<label class="polrow"><input type="radio" name="collisionPolicy" value="soft"><span><b>Ask each time</b> <span class="pmuted">— an "Open with…" prompt on conflict, remembered per name or per TLD.</span></span></label>
|
||||
</div>
|
||||
<label class="polrow" style="margin-top:4px"><input type="checkbox" id="bnsSameHostSubresource"><span><b>Same-host subresource routing</b> <span class="pmuted">— a page on <code>bns://name</code> fetching <code>https://name/…</code> for XHR/fetch/images gets routed through the BNS pinned endpoint (cookies, CORS, TLS all under one origin). Top-level navigation and other hosts are untouched; WebSocket upgrades stay on <code>wss://</code>.</span></span></label>
|
||||
</div>
|
||||
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
|
||||
<div class="txt"><div class="t">Remembered choices</div>
|
||||
|
|
@ -2527,6 +2528,11 @@
|
|||
cmDev.checked = s.contextMenuDevTools !== false;
|
||||
cmDev.addEventListener("change", () => { C.set("contextMenuDevTools", !!cmDev.checked); });
|
||||
}
|
||||
const bnsSH = document.getElementById("bnsSameHostSubresource");
|
||||
if (bnsSH) {
|
||||
bnsSH.checked = s.bnsSameHostSubresource !== false;
|
||||
bnsSH.addEventListener("change", () => { C.set("bnsSameHostSubresource", !!bnsSH.checked); });
|
||||
}
|
||||
|
||||
// ---- Plug-ins: Ariadne's Thread + Aegis --------------------------------
|
||||
// Two views per plug-in that share one truth source:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue