import { resolve, extname } from 'path'; import { readFile } from 'fs/promises'; import type { IncomingMessage, ServerResponse } from 'http'; /** Supported content types by file extension. */ const CONTENT_TYPES: Record = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.mjs': 'application/javascript', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon', '.woff2': 'font/woff2', }; /** * Serve static files from a directory. * * - Only handles GET requests * - Maps `/` to `/index.html` * - Protects against path traversal (resolved path must be within uiDir) * - Returns true if a file was served, false otherwise (caller should 404) */ export async function serveStatic( req: IncomingMessage, res: ServerResponse, uiDir: string, ): Promise { // Only serve GET requests if (req.method !== 'GET') { return false; } // Parse the pathname from the request URL const urlStr = req.url ?? '/'; let pathname: string; try { // Use URL constructor to safely parse; base doesn't matter for pathname extraction const parsed = new URL(urlStr, 'http://localhost'); pathname = parsed.pathname; } catch { return false; } // Map root to index.html if (pathname === '/') { pathname = '/index.html'; } // Check that the file extension is one we serve const ext = extname(pathname); const contentType = CONTENT_TYPES[ext]; if (!contentType) { return false; } // Resolve the absolute path to the requested file // Strip leading slash to get a relative path for resolve() const relativePath = pathname.slice(1); const resolvedUiDir = resolve(uiDir); const filePath = resolve(resolvedUiDir, relativePath); // Path traversal protection: ensure the resolved path is within uiDir if (!filePath.startsWith(resolvedUiDir + '/') && filePath !== resolvedUiDir) { return false; } // Try to read and serve the file try { const content = await readFile(filePath); res.writeHead(200, { 'Content-Type': contentType }); res.end(content); return true; } catch { // File doesn't exist or can't be read — let the caller handle it return false; } }