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("