Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596import { createReadStream, realpathSync, statSync } from "node:fs";import { createServer, type ServerResponse } from "node:http";import type { AddressInfo } from "node:net";import { extname, join, sep } from "node:path";
export interface FixtureServer { port: number; origin: string; close(): Promise<void>;}
const TYPES: Record<string, string> = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".txt": "text/plain; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".gif": "image/gif",};
const end = (res: ServerResponse, status: number, headers: Record<string, string> = {}) => { res.writeHead(status, headers); res.end();};
/** The real path of `candidate`, if it exists and lies inside `realRoot` (symlinks are resolved first). */function inside(realRoot: string, candidate: string): string | undefined { try { const real = realpathSync(candidate); return real === realRoot || real.startsWith(realRoot + sep) ? real : undefined; } catch { return undefined; }}
/** * Serves a folder of test pages on 127.0.0.1. Read-only, and it never serves anything outside the folder: * the path is decoded once, `..` and NUL and backslashes are refused, dot files are hidden, and symlinks are * only followed when they stay inside. */export async function startFixtureServer(options: { root: string; port: number; onRequest?: (path: string) => void }): Promise<FixtureServer> { let realRoot: string; try { if (!statSync(options.root).isDirectory()) throw new Error("not a folder"); realRoot = realpathSync(options.root); } catch { throw new Error(`${options.root} is not a folder`); }
const server = createServer((req, res) => { if (req.method !== "GET" && req.method !== "HEAD") return end(res, 405, { Allow: "GET, HEAD" }); let pathname: string; try { pathname = decodeURIComponent((req.url ?? "/").split(/[?#]/)[0]!); } catch { return end(res, 400); } options.onRequest?.(pathname); // for tests: which pages did the page ask for (a page can "report" a click by fetching a path) if (pathname.includes("\0") || pathname.includes("\\")) return end(res, 400); const segments = pathname.split("/").filter((s) => s !== ""); if (segments.includes("..")) return end(res, 400); if (segments.some((s) => s.startsWith("."))) return end(res, 404);
let file = inside(realRoot, join(realRoot, ...segments)); if (file && statSync(file).isDirectory()) file = inside(realRoot, join(file, "index.html")); if (!file || !statSync(file).isFile()) return end(res, 404);
const size = statSync(file).size; res.writeHead(200, { "Content-Type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream", "Content-Length": String(size), "Cache-Control": "no-store" }); if (req.method === "HEAD") return void res.end(); createReadStream(file).on("error", () => res.destroy()).pipe(res); });
await new Promise<void>((resolve, reject) => { server.once("error", (err: NodeJS.ErrnoException) => reject(err.code === "EADDRINUSE" ? new Error(`port ${options.port} is already in use`) : err)); server.listen(options.port, "127.0.0.1", resolve); }); const port = (server.address() as AddressInfo).port; let closed = false; return { port, origin: `http://127.0.0.1:${port}`, close: () => closed ? Promise.resolve() : new Promise<void>((resolve) => { closed = true; server.close(() => resolve()); server.closeAllConnections(); }), };}