import http from "node:http"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const MIME_TYPES = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript; charset=utf-8", ".mjs": "application/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".xml": "application/xml; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif", ".ico": "image/x-icon", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".txt": "text/plain; charset=utf-8" }; /** * Creates the HTTP server instance for serving static files from _site * @param {string} [customSiteDir] Optional custom directory to serve from * @returns {http.Server} */ export function createServer(customSiteDir) { const siteDir = path.resolve(customSiteDir || path.join(process.cwd(), "_site")); return http.createServer((req, res) => { // Standard security headers res.setHeader("X-Content-Type-Options", "nosniff"); res.setHeader("X-Frame-Options", "DENY"); res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); const parsedUrl = new URL(req.url, "http://localhost"); // Health check endpoint for container orchestrators & telemetry if (parsedUrl.pathname === "/healthz" || parsedUrl.pathname === "/health") { res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); res.end(JSON.stringify({ status: "ok", uptime: process.uptime() })); return; } // Path normalization & security validation against directory traversal let decodedPath = ""; try { decodedPath = decodeURIComponent(parsedUrl.pathname); } catch { res.writeHead(400, { "Content-Type": "text/plain" }); res.end("Bad Request"); return; } const resolvedPath = path.resolve(siteDir, `.${decodedPath}`); // Boundary check: ensure target path is within siteDir if (!resolvedPath.startsWith(siteDir)) { res.writeHead(403, { "Content-Type": "text/plain" }); res.end("Forbidden"); return; } let targetFile = resolvedPath; // Directory index resolution if (fs.existsSync(targetFile) && fs.statSync(targetFile).isDirectory()) { targetFile = path.join(targetFile, "index.html"); } // Clean URL extensionless fallback (/posts/my-post -> /posts/my-post.html) if (!fs.existsSync(targetFile) && fs.existsSync(`${targetFile}.html`)) { targetFile = `${targetFile}.html`; } // Check if target file exists and is a regular file if (fs.existsSync(targetFile) && fs.statSync(targetFile).isFile()) { const ext = path.extname(targetFile).toLowerCase(); const contentType = MIME_TYPES[ext] || "application/octet-stream"; // Cache control: assets get long-term caching, html gets short if (decodedPath.startsWith("/assets/")) { res.setHeader("Cache-Control", "public, max-age=2592000, immutable"); } else { res.setHeader("Cache-Control", "public, max-age=0, must-revalidate"); } res.writeHead(200, { "Content-Type": contentType }); const stream = fs.createReadStream(targetFile); stream.pipe(res); return; } // 404 Not Found fallback const custom404 = path.join(siteDir, "404.html"); if (fs.existsSync(custom404)) { res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" }); fs.createReadStream(custom404).pipe(res); } else { res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); res.end("404 Not Found"); } }); } // Direct CLI execution handler const currentFilePath = fileURLToPath(import.meta.url); if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) { const port = parseInt(process.env.PORT || "8080", 10); const host = process.env.HOST || "0.0.0.0"; const siteDir = path.resolve(process.cwd(), "_site"); if (!fs.existsSync(siteDir)) { console.warn(`[Static Server] Warning: '${siteDir}' does not exist yet. Run 'npm run build' first.`); } const server = createServer(); server.listen(port, host, () => { console.log(`[Static Server] Serving '${siteDir}' on http://${host}:${port}`); }); // Graceful termination handling const shutdown = (signal) => { console.log(`\n[Static Server] Received ${signal}. Shutting down gracefully...`); server.close(() => { process.exit(0); }); }; process.on("SIGINT", () => shutdown("SIGINT")); process.on("SIGTERM", () => shutdown("SIGTERM")); }