New record kind alongside h / s3 / ip / u: `p` reverse-proxies the request to a full upstream URL while keeping the BNS host in the address bar. Unlike `ip` (which pins the upstream to a raw IP + on-chain TLS fingerprint), `p` uses the upstream's own DNS + public CA cert and sends `Host:` of the upstream so vhost-based origins answer correctly. Argus (record-picker.js): - Placed AFTER `ip` in the apex chain: a name carrying both keeps its existing pinned-IP behavior; names with only `p` get honored instead of falling through to `u` and 302-ing away. - Not applied to subdomain inheritance — `p` is single-URL by intent, and inheriting through a subdomain would silently mangle the target. Theseus (main.js serveBns): - Fetches the upstream (path prefix from the `p` URL is preserved) and returns the response body/status/content-type verbatim. Provenance label "mirror" appears in the source badge. Tests: record-picker.test.mjs covers `p`-only, `p`+ip precedence, and the subdomain-suppression rule.
171 lines
8.4 KiB
JavaScript
171 lines
8.4 KiB
JavaScript
// The record-picker rule. Extracted from the 2026-08-13 subdomain-Sia bug:
|
|
// `checkers.game.x` inherits the parent `game.x`'s records (which include BOTH
|
|
// `s3` and `ip`); the gateway and Theseus were preferring `s3`, constructing a
|
|
// Sia key from the subdomain name, and serving a NoSuchKey XML error to every
|
|
// visitor. The parent's `ip` — configured on the VPS to Host-route the
|
|
// subdomain — was ignored.
|
|
//
|
|
// These tests assert the rule that fixes it, not the code that implements it.
|
|
// They will catch any regression that reintroduces s3-first-for-subdomains, and
|
|
// they will fail on any future change to `pickRecord` that violates a stated
|
|
// invariant even if the change looks locally reasonable.
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { pickRecord, isSubdomainOf } from "../src/lib/record-picker.js";
|
|
|
|
// A convenience — the exact bug case, in one place, so a search for it lands here.
|
|
const gameXRecords = {
|
|
ip: "130.12.242.84",
|
|
tls: "6af5f7cb3715e3b81bbabb51d0947cc8611a3e66ad7bc7940a447c2f66deec20",
|
|
s3: "bns/game/",
|
|
};
|
|
|
|
// ---------- isSubdomainOf ----------
|
|
|
|
test("isSubdomainOf: apex is not a subdomain of itself", () => {
|
|
assert.equal(isSubdomainOf("game.x", "game.x"), false);
|
|
assert.equal(isSubdomainOf("silentmode.bch", "silentmode.bch"), false);
|
|
});
|
|
|
|
test("isSubdomainOf: any label above the registered name counts as a subdomain", () => {
|
|
assert.equal(isSubdomainOf("checkers.game.x", "game.x"), true);
|
|
assert.equal(isSubdomainOf("blog.alice.p2p", "alice.p2p"), true);
|
|
assert.equal(isSubdomainOf("a.b.c.alice.p2p", "alice.p2p"), true);
|
|
});
|
|
|
|
test("isSubdomainOf: case-insensitive, so a badly-cased Host header does not fool it", () => {
|
|
assert.equal(isSubdomainOf("Game.X", "game.x"), false);
|
|
assert.equal(isSubdomainOf("Checkers.Game.X", "game.x"), true);
|
|
});
|
|
|
|
test("isSubdomainOf: a missing entry name means no inheritance decision to make", () => {
|
|
assert.equal(isSubdomainOf("anything.x", ""), false);
|
|
assert.equal(isSubdomainOf("anything.x", undefined), false);
|
|
});
|
|
|
|
// ---------- pickRecord: the regression from 2026-08-13 ----------
|
|
|
|
test("SUBDOMAIN with parent's {s3, ip}: picks ip — this is the 2026-08-13 fix", () => {
|
|
// The exact wrong behaviour was picking s3 here.
|
|
assert.equal(pickRecord(gameXRecords, { isSubdomain: true }), "ip");
|
|
});
|
|
|
|
test("APEX with the same {s3, ip}: picks s3 — the previous behaviour is unchanged", () => {
|
|
// If this ever flips, the apex Sia landing page (game.x itself) breaks.
|
|
assert.equal(pickRecord(gameXRecords, { isSubdomain: false }), "s3");
|
|
});
|
|
|
|
// ---------- pickRecord: full priority ordering ----------
|
|
|
|
test("apex priority is h → s3 → ip → u — matches the pre-2026-08-13 behaviour", () => {
|
|
const isSubdomain = false;
|
|
assert.equal(pickRecord({ h: "<b>hi</b>", s3: "bns/x/", ip: "1.2.3.4", u: "https://x" }, { isSubdomain }), "h");
|
|
assert.equal(pickRecord({ s3: "bns/x/", ip: "1.2.3.4", u: "https://x" }, { isSubdomain }), "s3");
|
|
assert.equal(pickRecord({ ip: "1.2.3.4", u: "https://x" }, { isSubdomain }), "ip");
|
|
assert.equal(pickRecord({ u: "https://x" }, { isSubdomain }), "u");
|
|
});
|
|
|
|
test("subdomain: ip wins over s3 AND over h — inheritance via Host, not per-name content", () => {
|
|
// Parent's `h` would render the same tiny HTML for every subdomain. That's
|
|
// the same class of wrong-behaviour as inherited `s3`. So subdomain-with-ip
|
|
// wins over `h` too.
|
|
const isSubdomain = true;
|
|
assert.equal(pickRecord({ h: "<b>hi</b>", ip: "1.2.3.4" }, { isSubdomain }), "ip");
|
|
assert.equal(pickRecord({ s3: "bns/x/", ip: "1.2.3.4" }, { isSubdomain }), "ip");
|
|
assert.equal(pickRecord({ h: "<b>hi</b>", s3: "bns/x/", ip: "1.2.3.4" }, { isSubdomain }), "ip");
|
|
});
|
|
|
|
// ---------- pickRecord: does NOT break Sia-per-subdomain owners ----------
|
|
|
|
test("subdomain, parent has ONLY s3 (no ip): still picks s3 — Sia-per-subdomain works", () => {
|
|
// An owner who wants every *.foo.p2p served from Sia should either register
|
|
// each subdomain with its own s3, or simply not publish `ip` on the parent.
|
|
// This asserts the second case still routes to Sia — the fix is targeted at
|
|
// the s3+ip case, not a broad "never inherit s3" rule.
|
|
const isSubdomain = true;
|
|
assert.equal(pickRecord({ s3: "bns/foo/" }, { isSubdomain }), "s3");
|
|
assert.equal(pickRecord({ h: "<b>hi</b>" }, { isSubdomain }), "h");
|
|
assert.equal(pickRecord({ u: "https://x" }, { isSubdomain }), "u");
|
|
});
|
|
|
|
// ---------- pickRecord: edge cases ----------
|
|
|
|
test("empty records → null (caller renders the entry as JSON, or 404s)", () => {
|
|
assert.equal(pickRecord({}, { isSubdomain: false }), null);
|
|
assert.equal(pickRecord({}, { isSubdomain: true }), null);
|
|
});
|
|
|
|
test("null/undefined records → null (do not throw)", () => {
|
|
assert.equal(pickRecord(null, { isSubdomain: false }), null);
|
|
assert.equal(pickRecord(undefined, { isSubdomain: true }), null);
|
|
});
|
|
|
|
test("missing options default to apex behaviour — safe default is not the fix path", () => {
|
|
// If a caller forgets to pass isSubdomain, we must not silently apply the
|
|
// subdomain rule; that could hide bugs where isSubdomain was miscomputed.
|
|
assert.equal(pickRecord(gameXRecords), "s3");
|
|
});
|
|
|
|
test("unrelated fields (tls, r, arbitrary) do not affect the pick", () => {
|
|
const records = { tls: "abc123", r: "phishing", extra: "ignored", ip: "1.2.3.4" };
|
|
assert.equal(pickRecord(records, { isSubdomain: false }), "ip");
|
|
assert.equal(pickRecord(records, { isSubdomain: true }), "ip");
|
|
});
|
|
|
|
// ---------- pickRecord: the `p` record (reverse proxy), added 2026-08-31 ----------
|
|
|
|
test("apex with ONLY p → picks p (not fall through to u or null)", () => {
|
|
// The reason `p` was added: a name that just wants to mirror an upstream
|
|
// via a full URL. Before, this fell through the ordering unnoticed and
|
|
// the gateway JSON-dumped the entry.
|
|
assert.equal(pickRecord({ p: "https://syskypo.de" }, { isSubdomain: false }), "p");
|
|
});
|
|
|
|
test("apex with s3 + p → s3 wins (owned bucket beats proxy)", () => {
|
|
// If the owner uploaded content, they meant that content. `p` is the
|
|
// "I don't have static content, mirror this instead" case.
|
|
assert.equal(pickRecord({ s3: "bns/x/", p: "https://x" }, { isSubdomain: false }), "s3");
|
|
});
|
|
|
|
test("apex with ip + p → ip wins (backward compat for on-chain-pinned proxies)", () => {
|
|
// coinspectrum.bch has both {ip, p, tls}. Its historical behaviour is
|
|
// "pin the upstream via chain-committed TLS fingerprint on the raw IP" —
|
|
// stricter than public-CA proxying via `p`. Do NOT flip that under it.
|
|
assert.equal(pickRecord({ ip: "1.2.3.4", p: "https://x", tls: "abc" }, { isSubdomain: false }), "ip");
|
|
});
|
|
|
|
test("apex with p + u → p wins (mirror beats redirect)", () => {
|
|
// p keeps the BNS address bar; u throws the visitor to the upstream URL.
|
|
// If the owner has both, they meant "mirror" — u is a fallback for clients
|
|
// that can't proxy.
|
|
assert.equal(pickRecord({ p: "https://x", u: "https://x" }, { isSubdomain: false }), "p");
|
|
});
|
|
|
|
test("subdomain inheritance does NOT use parent's p — p is single-URL by intent", () => {
|
|
// A parent with only `p` proxies its own content to one URL. Silently
|
|
// inheriting that for a subdomain query would route the subdomain's traffic
|
|
// through an upstream that doesn't know about it — usually the wrong thing.
|
|
// If the owner wants subdomain mirroring, they set `ip` (Host-routed).
|
|
assert.equal(pickRecord({ p: "https://x" }, { isSubdomain: true }), null);
|
|
assert.equal(pickRecord({ p: "https://x", u: "https://u" }, { isSubdomain: true }), "u");
|
|
});
|
|
|
|
// ---------- The invariant that must hold across any future change ----------
|
|
|
|
test("INVARIANT: for any records with both s3 and ip, the pick differs between apex and subdomain", () => {
|
|
// This is the core property of the fix. If some future edit makes them the
|
|
// same, the 2026-08-13 bug has been reintroduced regardless of what the
|
|
// ordering table now says.
|
|
const cases = [
|
|
{ s3: "bns/a/", ip: "1.1.1.1" },
|
|
{ s3: "bns/a/", ip: "1.1.1.1", u: "https://x" },
|
|
{ s3: "bns/a/", ip: "1.1.1.1", tls: "abc" },
|
|
];
|
|
for (const r of cases) {
|
|
const apex = pickRecord(r, { isSubdomain: false });
|
|
const sub = pickRecord(r, { isSubdomain: true });
|
|
assert.notEqual(apex, sub, `apex ${apex} === subdomain ${sub} for ${JSON.stringify(r)}`);
|
|
assert.equal(sub, "ip", "the subdomain path must pick ip when it is available");
|
|
}
|
|
});
|