100 lines
6.3 KiB
JavaScript
100 lines
6.3 KiB
JavaScript
|
|
// Blocklist harness — exercises the blocklist policy through the REAL
|
||
|
|
// serveBns handler and the real decision functions in main.js, without
|
||
|
|
// starting the app (so no updater, no Ariadne, no real profile).
|
||
|
|
//
|
||
|
|
// set THESEUS_USER_DATA=<scratch dir>
|
||
|
|
// npx electron dev/blocklist-selftest.js [name] (default: hello.bch)
|
||
|
|
//
|
||
|
|
// The flags normally arrive from the indexer process, which reads the lists
|
||
|
|
// from the chain; here the harness delivers them through the same message
|
||
|
|
// handler. It checks:
|
||
|
|
// 1. an unflagged name is served as before
|
||
|
|
// 2. a flagged name gets the inline warning from the protocol handler (403)
|
||
|
|
// 3. "continue anyway" is remembered per name, and only for a flagged name
|
||
|
|
// 4. policy "refuse" ignores that choice; reason "csam" never offers one;
|
||
|
|
// policy "off" ignores the lists
|
||
|
|
// 5. blocked.html renders the flag and shows/hides "continue" as told
|
||
|
|
// Prints a JSON report and exits 0 only if every check passed.
|
||
|
|
process.env.THESEUS_NO_AUTOSTART = "1";
|
||
|
|
if (!process.env.THESEUS_USER_DATA) { console.log("refusing to run without THESEUS_USER_DATA (it would use the real profile)"); process.exit(2); }
|
||
|
|
const { app, BrowserWindow, protocol } = require("electron");
|
||
|
|
const { serveBns, _blocklistTest: B } = require("../main.js");
|
||
|
|
const fs = require("fs");
|
||
|
|
const path = require("path");
|
||
|
|
|
||
|
|
app.disableHardwareAcceleration();
|
||
|
|
app.commandLine.appendSwitch("disable-gpu");
|
||
|
|
app.commandLine.appendSwitch("no-sandbox");
|
||
|
|
|
||
|
|
const name = (process.argv[2] || "hello.bch").toLowerCase();
|
||
|
|
const outDir = path.join(__dirname, "..", "dev-out");
|
||
|
|
const watchdog = setTimeout(() => { console.log("WATCHDOG: timed out"); try { app.exit(3); } catch {} }, 90000);
|
||
|
|
const checks = [];
|
||
|
|
const check = (what, ok, detail) => { checks.push({ what, ok: !!ok, ...(detail !== undefined ? { detail } : {}) }); };
|
||
|
|
const FLAG = (reason) => ({ name, reason, reportTxid: "ab".repeat(32), txid: "cd".repeat(32), height: 1, source: "test-list.x" });
|
||
|
|
|
||
|
|
app.whenReady().then(async () => {
|
||
|
|
protocol.handle("bns", serveBns);
|
||
|
|
const win = new BrowserWindow({ width: 1000, height: 700, show: false, webPreferences: { offscreen: true } });
|
||
|
|
const wc = win.webContents;
|
||
|
|
const titleOf = async (url) => { try { await wc.loadURL(url); } catch {} await new Promise((r) => setTimeout(r, 1500)); return wc.getTitle(); };
|
||
|
|
const statusOf = async (url) => (await serveBns(new Request(url))).status;
|
||
|
|
const WARN = `Warning: ${name} is flagged`;
|
||
|
|
|
||
|
|
// 1. unflagged
|
||
|
|
const plainTitle = await titleOf(`bns://${name}/`);
|
||
|
|
check("unflagged name is served as before", plainTitle !== WARN && (await statusOf(`bns://${name}/`)) !== 403, plainTitle);
|
||
|
|
check("no decision without a flag", B.decision({ name }, name) === null);
|
||
|
|
|
||
|
|
// 2. flagged
|
||
|
|
B.setFlags([FLAG("malware")]);
|
||
|
|
const d = B.decision({ name }, name);
|
||
|
|
check("flagged name: decision offers continue under the default policy", d && d.canContinue === true && d.flag.reason === "malware");
|
||
|
|
check("a subdomain is governed by its parent's flag", !!B.decision({ name }, "www." + name));
|
||
|
|
check("protocol handler answers 403 for a flagged name", (await statusOf(`bns://${name}/`)) === 403);
|
||
|
|
check("…and for its subresources", (await statusOf(`bns://${name}/style.css`)) === 403);
|
||
|
|
check("the inline warning is what renders", (await titleOf(`bns://${name}/`)) === WARN);
|
||
|
|
const inline = await wc.executeJavaScript("document.body.innerHTML");
|
||
|
|
check("the inline warning has no way to approve itself", !/block-choose|<button|<a /i.test(inline));
|
||
|
|
|
||
|
|
// 3. continue anyway
|
||
|
|
check("continuing is refused for a name that is not flagged", B.remember("not-flagged.x") === false);
|
||
|
|
check("continuing is remembered for the flagged name", B.remember(name) === true);
|
||
|
|
check("after that the name opens", B.decision({ name }, name) === null && (await statusOf(`bns://${name}/`)) !== 403);
|
||
|
|
const saved = JSON.parse(fs.readFileSync(path.join(app.getPath("userData"), "blocklist.json"), "utf8"));
|
||
|
|
check("the choice is persisted per name", !!saved.byName[name] && saved.byName[name].reason === "malware", Object.keys(saved.byName));
|
||
|
|
|
||
|
|
// 4. policies
|
||
|
|
B.setPolicy("refuse");
|
||
|
|
const r = B.decision({ name }, name);
|
||
|
|
check('policy "refuse" ignores an earlier "continue"', r && r.canContinue === false);
|
||
|
|
check('…and "continue" cannot be recorded under it', B.remember(name) === false);
|
||
|
|
B.setPolicy("off");
|
||
|
|
check('policy "off" ignores the lists', B.decision({ name }, name) === null);
|
||
|
|
B.setPolicy("warn"); B.forget();
|
||
|
|
B.setFlags([FLAG("csam")]);
|
||
|
|
const c = B.decision({ name }, name);
|
||
|
|
check("reason csam is never offered a way through", c && c.canContinue === false && B.remember(name) === false);
|
||
|
|
B.setFlags([]);
|
||
|
|
check("an emptied list lifts the flag", B.decision({ name }, name) === null);
|
||
|
|
|
||
|
|
// 5. the interstitial page
|
||
|
|
const page = (proceed) => ({ search: new URLSearchParams({ host: name, name, reason: "phishing", list: "test-list.x", report: "ab".repeat(32), resturl: "/", proceed }).toString() });
|
||
|
|
await wc.loadFile(path.join(__dirname, "..", "blocked.html"), page("1"));
|
||
|
|
await new Promise((r2) => setTimeout(r2, 800));
|
||
|
|
const withGo = await wc.executeJavaScript(`({ title: document.title, name: document.getElementById("name").textContent, why: document.getElementById("why").textContent, go: !document.getElementById("go").hidden, refused: !document.getElementById("refused").hidden, list: document.getElementById("list").textContent })`);
|
||
|
|
check("blocked.html shows the flag", withGo.title === WARN && withGo.name === name && /imitates another site/.test(withGo.why) && withGo.list === "test-list.x", withGo);
|
||
|
|
check("blocked.html offers continue when told it may", withGo.go === true && withGo.refused === false);
|
||
|
|
fs.mkdirSync(outDir, { recursive: true });
|
||
|
|
try { fs.writeFileSync(path.join(outDir, "blocked.png"), (await wc.capturePage()).toPNG()); } catch {}
|
||
|
|
await wc.loadFile(path.join(__dirname, "..", "blocked.html"), page("0"));
|
||
|
|
await new Promise((r2) => setTimeout(r2, 800));
|
||
|
|
const noGo = await wc.executeJavaScript(`({ go: !document.getElementById("go").hidden, refused: !document.getElementById("refused").hidden })`);
|
||
|
|
check("blocked.html hides continue when refused", noGo.go === false && noGo.refused === true);
|
||
|
|
|
||
|
|
const failed = checks.filter((x) => !x.ok);
|
||
|
|
console.log(JSON.stringify({ name, passed: checks.length - failed.length, failed: failed.length, checks }, null, 1));
|
||
|
|
clearTimeout(watchdog);
|
||
|
|
app.exit(failed.length ? 1 : 0);
|
||
|
|
});
|