import {mkdirSync, writeFileSync} from "node:fs"; import {createServer, type Server} from "node:http"; import {type AddressInfo, connect} from "node:net"; import {join} from "node:path"; import {brotliCompressSync, gzipSync} from "node:zlib"; import tmp from "tmp"; import {afterAll, beforeAll, describe, expect, it, vi} from "vitest"; import {createStaticFileServer, type sendNotFound} from "../lib/util/staticFileServer"; const INDEX_HTML = "index"; const APP_JS = `console.log("${"x".repeat(2048)}");`; /** Written next to the served directory, never inside it, so a traversal that succeeds is actually observable. */ const SECRET = "topsecret-must-never-be-served"; const mockLogError = vi.fn<(message: string) => void>(); let dir: string; let server: Server; let baseUrl: string; /** Starts a `node:http` server serving `dir`, mirroring how the frontend/onboarding extensions wire it up. */ function listen(handler: (request: Parameters[0], response: Parameters[1]) => void): Promise { server = createServer(handler); return new Promise((resolve) => { server.listen(0, "127.0.0.1", () => { baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; resolve(); }); }); } /** Writes a request line verbatim, bypassing the path normalization `fetch` applies before sending. */ function rawRequest(target: string): Promise { return new Promise((resolve, reject) => { const socket = connect((server.address() as AddressInfo).port, "127.0.0.1", () => { socket.write(`GET ${target} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n`); }); let received = ""; socket.setEncoding("utf8"); socket.on("data", (chunk) => { received += chunk; }); socket.on("end", () => resolve(received)); socket.on("error", reject); }); } describe("StaticFileServer", () => { beforeAll(async () => { const root = tmp.dirSync().name; dir = join(root, "public"); // outside the served directory: only a working traversal could reach it writeFileSync(join(root, "secret.txt"), SECRET); mkdirSync(join(dir, "sub"), {recursive: true}); writeFileSync(join(dir, "index.html"), INDEX_HTML); writeFileSync(join(dir, "app.js"), APP_JS); // precompressed variants, as shipped by the frontend packages writeFileSync(join(dir, "app.js.gz"), gzipSync(APP_JS)); writeFileSync(join(dir, "app.js.br"), brotliCompressSync(APP_JS)); writeFileSync(join(dir, "sub", "icon.png"), Buffer.from([0x89, 0x50, 0x4e, 0x47])); await listen(createStaticFileServer(dir, mockLogError)); }); afterAll(async () => { await new Promise((resolve) => server.close(resolve)); }); it("serves a file with its content type", async () => { const response = await fetch(`${baseUrl}/sub/icon.png`); expect(response.status).toStrictEqual(200); expect(response.headers.get("content-type")).toStrictEqual("image/png"); expect(response.headers.get("content-encoding")).toBeNull(); }); it("serves index.html for the root, never cached", async () => { const response = await fetch(`${baseUrl}/`); expect(response.status).toStrictEqual(200); expect(response.headers.get("content-type")).toStrictEqual("text/html; charset=utf-8"); expect(response.headers.get("cache-control")).toStrictEqual("no-store"); await expect(response.text()).resolves.toStrictEqual(INDEX_HTML); }); it("serves the precompressed brotli variant", async () => { const response = await fetch(`${baseUrl}/app.js`, {headers: {"Accept-Encoding": "br"}}); expect(response.status).toStrictEqual(200); expect(response.headers.get("content-encoding")).toStrictEqual("br"); expect(response.headers.get("content-type")).toStrictEqual("text/javascript; charset=utf-8"); expect(response.headers.get("vary")).toStrictEqual("Accept-Encoding"); // decoded by fetch, so the served bytes must be the brotli variant of the original file await expect(response.text()).resolves.toStrictEqual(APP_JS); }); it("serves the precompressed gzip variant", async () => { const response = await fetch(`${baseUrl}/app.js`, {headers: {"Accept-Encoding": "gzip"}}); expect(response.status).toStrictEqual(200); expect(response.headers.get("content-encoding")).toStrictEqual("gzip"); await expect(response.text()).resolves.toStrictEqual(APP_JS); }); it("serves the identity file when no encoding is accepted", async () => { const response = await fetch(`${baseUrl}/app.js`, {headers: {"Accept-Encoding": "identity"}}); expect(response.status).toStrictEqual(200); expect(response.headers.get("content-encoding")).toBeNull(); expect(response.headers.get("content-length")).toStrictEqual(String(Buffer.byteLength(APP_JS))); await expect(response.text()).resolves.toStrictEqual(APP_JS); }); it("revalidates with an etag", async () => { const response = await fetch(`${baseUrl}/app.js`, {headers: {"Accept-Encoding": "identity"}}); const etag = response.headers.get("etag"); expect(etag).toBeTruthy(); const revalidated = await fetch(`${baseUrl}/app.js`, {headers: {"Accept-Encoding": "identity", "If-None-Match": etag as string}}); expect(revalidated.status).toStrictEqual(304); }); it("returns 404 for an unknown file", async () => { const response = await fetch(`${baseUrl}/nope.js`); expect(response.status).toStrictEqual(404); expect(response.headers.get("content-type")).toStrictEqual("text/html; charset=utf-8"); expect(response.headers.get("content-security-policy")).toStrictEqual("default-src 'none'"); expect(response.headers.get("x-content-type-options")).toStrictEqual("nosniff"); await expect(response.text()).resolves.toContain("Cannot GET /nope.js"); }); it("escapes the url in the 404 body", async () => { const response = await fetch(`${baseUrl}/%3Cscript%3E`); expect(response.status).toStrictEqual(404); await expect(response.text()).resolves.not.toContain("