import http from 'http'; import fs from 'fs'; import path from 'path'; import url from 'url'; const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); const ROOT = process.env.WEB_ROOT || path.resolve(__dirname, 'dist'); const PORT = Number(process.env.PORT || 3000); const MIME = { '.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', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.gif': 'image/gif', '.ico': 'image/x-icon', '.txt': 'text/plain; charset=utf-8', '.map': 'application/json; charset=utf-8', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf', '.eot': 'application/vnd.ms-fontobject', '.mp4': 'video/mp4', '.webm': 'video/webm', '.wasm': 'application/wasm' }; function send(res, status, headers, stream) { res.writeHead(status, headers); if (stream) stream.pipe(res); else res.end(); } function isUnder(p, root) { const rel = path.relative(root, p); return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel); } const server = http.createServer((req, res) => { try { const parsed = url.parse(req.url || '/'); let pathname = decodeURIComponent(parsed.pathname || '/'); // Hard normalize, prevent traversal pathname = path.normalize(pathname).replace(/^([/\\])*|\/+$/g, '/'); let fp = path.join(ROOT, pathname); // If path is a directory, serve index.html if (fs.existsSync(fp) && fs.statSync(fp).isDirectory()) { fp = path.join(fp, 'index.html'); } // If it doesn't exist and path didn't include .html, try appending index.html if (!fs.existsSync(fp) && !path.extname(fp)) { fp = path.join(fp, 'index.html'); } // Verify containment in ROOT if (!isUnder(fp, ROOT) && path.resolve(fp) !== path.resolve(ROOT, 'index.html')) { return send(res, 403, { 'content-type': 'text/plain; charset=utf-8' }, null); } if (!fs.existsSync(fp) || fs.statSync(fp).isDirectory()) { return send(res, 404, { 'content-type': 'text/plain; charset=utf-8' }, null); } const ext = path.extname(fp).toLowerCase(); const type = MIME[ext] || 'application/octet-stream'; const headers = { 'content-type': type }; // Caching: long cache for assets, no-cache for html if (ext === '.html') { headers['cache-control'] = 'no-cache'; } else if (fp.includes('/_astro/') || fp.startsWith(path.join(ROOT, 'assets'))) { headers['cache-control'] = 'public, max-age=31536000, immutable'; } else { headers['cache-control'] = 'public, max-age=3600'; } const stream = fs.createReadStream(fp); stream.on('open', () => send(res, 200, headers, stream)); stream.on('error', () => send(res, 500, { 'content-type': 'text/plain; charset=utf-8' }, null)); } catch (err) { send(res, 500, { 'content-type': 'text/plain; charset=utf-8' }, null); } }); server.listen(PORT, '0.0.0.0', () => { // eslint-disable-next-line no-console console.log(`Static server listening on :${PORT}, root: ${ROOT}`); });