import { expect, test } from "@playwright/test"; // Wget-style denial: a non-browser client (curl/wget/WebDAV clients solve no // JS challenge) must never receive application bytes from a guarded service. // Today the guard chain answers every unauthenticated path — roots and deep // paths alike — with an HTML gate page, so a status-code-only assertion would // pass even if a route leaked: these tests assert on LEAKAGE instead. The // markers below live in APP code (copyparty's UI title, the homepage panel // grid, the registry JSON shape), so any guard-route regression that lets the // app answer directly fails here, while any gate implementation (challenge or // login flow) passes. // // Copyparty serves authenticated traffic with anonymous read perms (`r` for // all authenticated traffic; see dankserver/copyparty/quadlet), so the guard // chain is the ONLY thing between an unauthenticated client and the file // bytes — if any route bypasses the guard, copyparty answers with a real // directory listing and these tests fail. const FILES = "https://files.franksdank.cloud"; const BASE = "https://www.franksdank.cloud"; // Paths probed as an unauthenticated raw client. These are probes, not claims // that the path exists when authenticated: whatever lives behind a path, the // answer to an unauthenticated raw client must be the gate — never app bytes. const FILE_PATHS = ["/", "/w/", "/cfg/", "/dank/", "/favicon.ico", "/robots.txt"]; test("wget-style GETs of copyparty paths never return file bytes to unauthenticated clients", async ({ request }) => { for (const p of FILE_PATHS) { const resp = await request.get(`${FILES}${p}`, { timeout: 30_000 }); const body = await resp.text(); // The gate answers in HTML; a copyparty file/listing response would not. expect( resp.headers()["content-type"] ?? "", `${p}: content-type must stay text/html while unauthenticated`, ).toMatch(/text\/html/); // No leakage: copyparty's UI markers must not appear anywhere in the body. expect(body, `${p}: copyparty bytes leaked to an unauthenticated client`).not.toMatch(/copyparty/i); } }); test("homepage app bytes are never served to unauthenticated clients", async ({ request }) => { // The homepage HTML itself: an unauthenticated GET must render the gate, // not the panel grid. const root = await request.get(`${BASE}/`, { timeout: 30_000 }); expect(await root.headers()).toBeTruthy(); expect((await root.text()), "homepage app markup leaked to an unauthenticated client").not.toContain('id="grid"'); // The services registry JSON: the inventory of internal services must never // be served pre-auth — not even as a "harmless" static file. const registry = await request.get(`${BASE}/services.json`, { timeout: 30_000 }); const ctype = (registry.headers()["content-type"] ?? "") as string; expect(ctype, "services.json must not be served as JSON to unauthenticated clients").not.toMatch(/application\/json/); expect(await registry.text(), "services registry leaked to an unauthenticated client").not.toContain('"category"'); // Admin panel: the app's own RBAC denial page is itself app output — even // that must not reach an unauthenticated client; the gate answers first. const admin = await request.get(`${BASE}/admin/`, { timeout: 30_000 }); expect( await admin.text(), "the homepage admin panel answered before the guard chain did", ).not.toContain("homepage admins only"); });