140 lines
4.5 KiB
JavaScript
140 lines
4.5 KiB
JavaScript
const fs = require('node:fs');
|
|
const http = require('node:http');
|
|
const path = require('node:path');
|
|
|
|
const defaultRoot = path.resolve(__dirname, '..', 'dist');
|
|
const mimeTypes = Object.freeze({
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.js': 'text/javascript; charset=utf-8',
|
|
'.json': 'application/json; charset=utf-8',
|
|
'.png': 'image/png',
|
|
'.svg': 'image/svg+xml',
|
|
});
|
|
const securityHeaders = Object.freeze({
|
|
'Content-Security-Policy': "default-src 'self'; base-uri 'none'; connect-src 'none'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self'",
|
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
'Referrer-Policy': 'no-referrer',
|
|
'X-Content-Type-Options': 'nosniff',
|
|
'X-Frame-Options': 'DENY',
|
|
});
|
|
|
|
function writePlain(response, status, message, extraHeaders = {}) {
|
|
const body = `${message}\n`;
|
|
response.writeHead(status, {
|
|
...securityHeaders,
|
|
...extraHeaders,
|
|
'Cache-Control': 'no-store',
|
|
'Content-Length': Buffer.byteLength(body),
|
|
'Content-Type': 'text/plain; charset=utf-8',
|
|
});
|
|
response.end(body);
|
|
}
|
|
|
|
function createStaticServer({ root = defaultRoot } = {}) {
|
|
const directory = path.resolve(root);
|
|
return http.createServer((request, response) => {
|
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
writePlain(response, 405, 'Method not allowed', { Allow: 'GET, HEAD' });
|
|
return;
|
|
}
|
|
|
|
let pathname;
|
|
try {
|
|
pathname = decodeURIComponent(new URL(request.url, 'http://127.0.0.1').pathname);
|
|
} catch (_) {
|
|
writePlain(response, 400, 'Bad request');
|
|
return;
|
|
}
|
|
|
|
// Browsers probe this path even when the product intentionally ships no icon.
|
|
// A quiet no-content response prevents a false runtime error without adding
|
|
// an unreviewed visual asset to the approved interface.
|
|
if (pathname === '/favicon.ico') {
|
|
response.writeHead(204, {
|
|
...securityHeaders,
|
|
'Cache-Control': 'public, max-age=86400',
|
|
});
|
|
response.end();
|
|
return;
|
|
}
|
|
|
|
const relative = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
|
|
const file = path.resolve(directory, relative);
|
|
if (!file.startsWith(`${directory}${path.sep}`)) {
|
|
writePlain(response, 404, 'Not found');
|
|
return;
|
|
}
|
|
|
|
let stat;
|
|
try {
|
|
stat = fs.statSync(file);
|
|
} catch (_) {
|
|
writePlain(response, 404, 'Not found');
|
|
return;
|
|
}
|
|
if (!stat.isFile()) {
|
|
writePlain(response, 404, 'Not found');
|
|
return;
|
|
}
|
|
|
|
response.writeHead(200, {
|
|
...securityHeaders,
|
|
'Cache-Control': 'no-cache',
|
|
'Content-Length': stat.size,
|
|
'Content-Type': mimeTypes[path.extname(file).toLowerCase()] || 'application/octet-stream',
|
|
});
|
|
if (request.method === 'HEAD') {
|
|
response.end();
|
|
return;
|
|
}
|
|
fs.createReadStream(file).pipe(response);
|
|
});
|
|
}
|
|
|
|
function readOption(args, name, fallback) {
|
|
const index = args.indexOf(name);
|
|
if (index === -1) return fallback;
|
|
if (!args[index + 1] || args[index + 1].startsWith('--')) throw new Error(`${name} requires a value`);
|
|
return args[index + 1];
|
|
}
|
|
|
|
function startFromCli() {
|
|
const args = process.argv.slice(2);
|
|
const known = new Set(['--host', '--port']);
|
|
for (let index = 0; index < args.length; index += 2) {
|
|
if (!known.has(args[index])) throw new Error(`Unknown option: ${args[index]}`);
|
|
}
|
|
const host = readOption(args, '--host', process.env.TECH_ATLAS_HOST || '127.0.0.1');
|
|
const portText = readOption(args, '--port', process.env.TECH_ATLAS_PORT || '4173');
|
|
const port = Number(portText);
|
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`Invalid port: ${portText}`);
|
|
|
|
const server = createStaticServer();
|
|
server.on('error', error => {
|
|
console.error(`Technology Atlas server failed: ${error.message}`);
|
|
process.exitCode = 1;
|
|
});
|
|
server.listen(port, host, () => {
|
|
const address = server.address();
|
|
const actualPort = typeof address === 'object' && address ? address.port : port;
|
|
console.log(`Technology Atlas ready at http://${host}:${actualPort}`);
|
|
console.log('Public deep-dive: Transaction Isolation / Lost Update');
|
|
});
|
|
|
|
const stop = () => server.close(() => process.exit());
|
|
process.once('SIGINT', stop);
|
|
process.once('SIGTERM', stop);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
try {
|
|
startFromCli();
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
module.exports = { createStaticServer, defaultRoot, securityHeaders };
|