import { readFileSync, statSync } from "node:fs"; import { createSecureServer, constants as httpConstants } from "node:http2"; import path from "node:path"; const port = process.env.PORT ?? process.argv[2] ?? 8433; const serverRoot = process.env.SERVER_ROOT ?? "./app"; const keyPath = process.env.KEY_PATH ?? "./server.key"; const certPath = process.env.CERT_PATH ?? "./server.pem"; const hstsMaxAge = process.env.HSTS_MAX_AGE ?? "31536000"; const corsOrigin = process.env.CORS_ORIGIN ?? "*"; const mimeType = { ".css": "text/css", ".doc": "application/msword", ".eot": "application/vnd.ms-fontobject", ".html": "text/html", ".ico": "image/x-icon", ".jpg": "image/jpeg", ".js": "text/javascript", ".json": "application/json", ".mp3": "audio/mpeg", ".pdf": "application/pdf", ".png": "image/png", ".svg": "image/svg+xml", ".ttf": "application/x-font-ttf", ".wav": "audio/wav", ".zip": "application/zip", }; const { HTTP2_HEADER_PATH: HEADER_PATH, HTTP2_HEADER_METHOD: HEADER_METHOD, HTTP_STATUS_NOT_FOUND: STATUS_NOT_FOUND, HTTP_STATUS_INTERNAL_SERVER_ERROR: STATUS_SERVER_ERROR, } = httpConstants; const HEADER_ORIGIN = "origin"; /** * HTTP/2 secure server instance * @type {import('node:http2').Http2SecureServer} */ let server; try { server = createSecureServer({ cert: readFileSync(certPath), key: readFileSync(keyPath), }); } catch (error) { if (error && "code" in error && error.code === "ENOENT") { console.error(`Certificate error: Could not find key or cert files at ${keyPath}, ${certPath}`); console.error("Please set KEY_PATH and CERT_PATH environment variables"); process.exit(1); } throw error; } /** * Handles stream errors and sends appropriate HTTP responses * @param {unknown} err - The error object * @param {import('node:http2').ServerHttp2Stream} stream - The HTTP/2 stream to respond on * @param {string | undefined} origin - The request origin for CORS headers */ const handleStreamError = (err, stream, origin) => { console.error("Stream error:", err); let status = STATUS_SERVER_ERROR; if (err && "code" in err && typeof err.code === "string") { status = err.code === "ENOENT" ? STATUS_NOT_FOUND : STATUS_SERVER_ERROR; } const body = status === STATUS_NOT_FOUND ? "Not Found" : "Internal Server Error"; stream.respond({ ":status": status, ...getResponseHeaders("text/plain", origin), "content-length": body.length.toString(), }); stream.end(body); }; /** * Generates common response headers including HSTS and CORS * @param {string} mimeType - The MIME type for content-type header * @param {string|undefined} ro - The Origin header from the request * @returns {Object.} Object containing response headers */ const getResponseHeaders = (mimeType, ro) => { let acao = "*"; if (corsOrigin !== "*") { const allowed = corsOrigin.split(",").map((s) => s.trim()); acao = ro && allowed.includes(ro) ? ro : allowed[0] || ""; } return { "access-control-allow-credentials": "true", "access-control-allow-headers": "Content-Type, Authorization", "access-control-allow-methods": "GET, HEAD, OPTIONS", "access-control-allow-origin": acao, "content-type": mimeType, "strict-transport-security": `max-age=${hstsMaxAge}; includeSubDomains`, }; }; /** * Handles incoming HTTP/2 streams * @param {import('node:http2').ServerHttp2Stream} stream - The HTTP/2 stream * @param {import('node:http2').IncomingHttpHeaders} headers - Request headers * @param {number} flags - Stream flags */ server.on("stream", (stream, headers) => { const reqPath = headers[HEADER_PATH] || "/"; const reqMethod = headers[HEADER_METHOD] || "GET"; const requestOrigin = typeof headers[HEADER_ORIGIN] === "string" ? headers[HEADER_ORIGIN] : undefined; // Handle CORS preflight requests if (reqMethod === "OPTIONS") { stream.respond({ ":status": 204, ...getResponseHeaders("text/plain", requestOrigin), "content-length": "0", }); stream.end(); return; } // Only allow GET and HEAD methods if (!["GET", "HEAD"].includes(reqMethod)) { stream.respond({ ":status": 405, allow: "GET, HEAD, OPTIONS", ...getResponseHeaders("text/plain", requestOrigin), }); stream.end("Method Not Allowed"); return; } let fullPath = path.join(serverRoot, reqPath); try { const stats = statSync(fullPath); if (stats.isDirectory()) { fullPath = path.join(fullPath, "index.html"); } } catch (error) { handleStreamError(error, stream, requestOrigin); return; } const ext = path.extname(fullPath); const responseMimeType = mimeType[ext] || "text/plain"; stream.respondWithFile( fullPath, { ...getResponseHeaders(responseMimeType, requestOrigin) }, { onError: (err) => handleStreamError(err, stream, requestOrigin) }, ); }); /** * Handles graceful shutdown of the server * @param {string} signal - The signal that triggered shutdown (SIGTERM, SIGINT, etc.) */ const gracefulShutdown = (signal) => { console.log(`\nReceived ${signal}, shutting down gracefully...`); server.close((err) => { if (err) { console.error("Error during server shutdown:", err); process.exit(1); } console.log("Server closed successfully"); process.exit(0); }); // Force close after 10 seconds setTimeout(() => { console.error("Forcing server closure after timeout"); process.exit(1); }, 2000); }; process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); process.on("SIGINT", () => gracefulShutdown("SIGINT")); /** * Handles uncaught exceptions * @param {Error} error - The uncaught error */ process.on("uncaughtException", (error) => { console.error("Uncaught Exception:", error); gracefulShutdown("uncaughtException"); }); /** * Handles unhandled promise rejections * @param {any} reason - The rejection reason * @param {Promise} promise - The promise that was rejected */ process.on("unhandledRejection", (reason, promise) => { console.error("Unhandled Rejection at:", promise, "reason:", reason); gracefulShutdown("unhandledRejection"); }); /** * Starts the HTTP/2 server and begins listening for connections */ server.listen(parseInt(port, 10), "localhost", () => { console.log(` Server running on https://localhost:${port} Environment: ${process.env.NODE_ENV || "development"} Server root: ${serverRoot} CORS origin: ${corsOrigin} HSTS max-age: ${hstsMaxAge} `); });