scripts/test_csp_browser.mjs
Rama main · réplica de lectura del repositorio original.
import assert from "node:assert/strict";
import { randomBytes } from "node:crypto";
import { createServer } from "node:http";
import { createRequire } from "node:module";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
/** Baut die vorhandene Vorschau und prüft sie unter der tatsächlich konfigurierten CSP. */
export async function testCspBrowser(page) {
const { build } = await import("../frontend/node_modules/vite/dist/node/index.js");
const directory = mkdtempSync(path.join(root, ".csp-test-"));
const previous = process.cwd();
let server;
const errors = [];
const onConsole = (message) => {
if (/content.security.policy|violates.*directive|refused to (apply|load|execute|connect)/i.test(message.text())) errors.push(message.text());
};
const onPageError = (error) => errors.push(error.message);
page.on("console", onConsole);
page.on("pageerror", onPageError);
try {
process.chdir(path.join(root, "frontend"));
await build({
configFile: path.join(root, "frontend/vite.preview.config.ts"), configLoader: "runner", logLevel: "error",
build: { outDir: directory, rollupOptions: { input: path.join(root, "frontend/preview.html") } },
});
process.chdir(previous);
const source = readFileSync(path.join(root, "backend/src/nodvard_deck/main.py"), "utf8");
const template = [...source.match(/\nSPA_CSP = \(([\s\S]*?)\n\)/)[1].matchAll(/"([^"]*)"/g)].map((match) => match[1]).join("");
assert(!template.includes("unsafe-inline") && !template.includes("*"));
server = createServer((request, response) => {
const url = new URL(request.url, "http://localhost");
const filename = path.resolve(directory, "." + url.pathname);
if (!filename.startsWith(directory + path.sep)) { response.writeHead(404).end(); return; }
try {
let content = readFileSync(filename);
const type = path.extname(filename);
response.setHeader("Content-Type", ({ ".html": "text/html", ".js": "text/javascript", ".css": "text/css" })[type] ?? "application/octet-stream");
if (type === ".html") {
const nonce = randomBytes(32).toString("base64url");
content = Buffer.from(content.toString().replace("<head>", `<head><meta name="csp-nonce" content="${nonce}">`));
response.setHeader("Content-Security-Policy", template.replace("__DECK_CSP_NONCE__", nonce).replace("__DECK_CSP_SOCKET__", `ws://127.0.0.1:${server.address().port}`) + "; script-src 'self'; frame-ancestors 'none'");
response.setHeader("Cache-Control", "no-store");
}
response.end(content);
} catch { response.writeHead(404).end(); }
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
await page.addInitScript(() => {
window.cspViolations = [];
document.addEventListener("securitypolicyviolation", (event) => window.cspViolations.push(`${event.effectiveDirective}: ${event.blockedURI}`));
});
const results = [];
for (const [route, text] of [
["/", "Widgets"], ["/settings/appearance", "Darstellung"],
["/ext/docker/matrix", "Docker"], ["/ext/shield/soc", "Nodvard Shield"],
["/terminal", "Terminal"], ["/ext/documents/documents", "Dokumente"],
]) {
const response = await page.goto(`http://127.0.0.1:${server.address().port}/preview.html?path=${encodeURIComponent(route)}`);
assert.equal(response.status(), 200);
await page.getByRole("heading", { name: text, exact: true }).first().waitFor();
await page.waitForFunction(() => !document.body.textContent.includes("Lade Extension-Seite"));
if (route === "/terminal") {
await page.getByRole("button", { name: "Terminal öffnen", exact: true }).first().click();
await page.locator(".xterm-screen").waitFor();
}
assert.deepEqual(await page.evaluate(() => window.cspViolations), [], route);
assert.deepEqual(errors, [], route);
assert(await page.evaluate(() => [...document.querySelectorAll("style")].every((style) => style.nonce === document.querySelector('meta[name="csp-nonce"]').content)), route);
results.push(route);
}
return results;
} finally {
process.chdir(previous);
page.off("console", onConsole);
page.off("pageerror", onPageError);
if (server) await new Promise((resolve) => server.close(resolve));
assert(directory.startsWith(root + path.sep + ".csp-test-"));
rmSync(directory, { recursive: true, force: true });
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const { chromium } = createRequire(path.join(root, "frontend/package.json"))("playwright");
const browser = await chromium.launch();
try { console.log(await testCspBrowser(await browser.newPage())); }
finally { await browser.close(); }
}