Files
Infernal-Pinball/server.js
T
2026-09-12 01:33:07 -04:00

76 lines
2.4 KiB
JavaScript

// Zero-dependency static file server for the pinball game.
// Usage: `npm start` (or `PORT=8080 npm start`), then open the printed URL.
import http from 'node:http';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const PORT = Number(process.env.PORT) || 3000;
const HOST = process.env.HOST || '127.0.0.1';
const PUBLIC_DIR = fileURLToPath(new URL('./public', import.meta.url));
// Module scripts must be served with a JavaScript MIME type or the browser refuses to run them.
const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.ico': 'image/x-icon',
'.json': 'application/json; charset=utf-8',
};
function sendText(res, status, message) {
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(message);
}
const server = http.createServer(async (req, res) => {
if (req.method !== 'GET' && req.method !== 'HEAD') {
res.setHeader('Allow', 'GET, HEAD');
sendText(res, 405, 'Method not allowed');
return;
}
let pathname;
try {
pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
} catch {
sendText(res, 400, 'Bad request');
return;
}
if (pathname.endsWith('/')) pathname += 'index.html';
// Resolve inside PUBLIC_DIR and refuse anything that escapes it.
const filePath = path.join(PUBLIC_DIR, pathname);
if (!filePath.startsWith(PUBLIC_DIR + path.sep)) {
sendText(res, 403, 'Forbidden');
return;
}
try {
const body = await readFile(filePath);
const type = MIME_TYPES[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'no-cache' });
res.end(req.method === 'HEAD' ? undefined : body);
} catch (err) {
if (err.code === 'ENOENT' || err.code === 'EISDIR') sendText(res, 404, 'Not found');
else sendText(res, 500, 'Server error');
}
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${PORT} is already in use. Try: PORT=${PORT + 1} npm start`);
} else {
console.error(err);
}
process.exit(1);
});
server.listen(PORT, HOST, () => {
console.log(`Pinball is running at http://localhost:${PORT}`);
console.log('Press Ctrl+C to stop.');
});