diff --git a/run-command.mjs b/run-command.mjs index cf5c8601..62659fa5 100644 --- a/run-command.mjs +++ b/run-command.mjs @@ -22,7 +22,7 @@ const shutdown = fileURLToPath(new URL("./src/.shutdown", import.meta.url)); // Run each command line argument passed after node run-command.mjs. // Commands are defined in the switch case statement below. -for(let i = 2; i < process.argv.length; i++) +for (let i = 2; i < process.argv.length; i++) switch (process.argv[i]) { // Commmand to boot up the server. Use PM2 to run if production is true in the // config file. @@ -54,6 +54,9 @@ for(let i = 2; i < process.argv.length; i++) case "stop": await writeFile(shutdown, ""); try { +// Give the server 5 seconds to respond, otherwise cancel this and throw an +// error to the console. The fetch request will also throw an error immediately +// if checking the server on localhost and the port is unused. let timeoutId = undefined; const response = await Promise.race([ fetch(new URL("/test-shutdown", serverUrl)), @@ -64,8 +67,11 @@ for(let i = 2; i < process.argv.length; i++) }) ]); clearTimeout(timeoutId); - if(response === "Error") throw new Error("Server is unresponsive."); - } catch (e) {await unlink(shutdown)} + if (response === "Error") throw new Error("Server is unresponsive."); + } catch (e) { + console.error(e); + await unlink(shutdown); + } if (config.production) exec("npm run pm2-stop", (error, stdout) => { if (error) throw error; diff --git a/src/server.mjs b/src/server.mjs index 3fbd3d6f..3f6d21de 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -16,8 +16,14 @@ import { paintSource, tryReadFile } from './randomization.mjs'; import loadTemplates from './templates.mjs'; import { fileURLToPath } from 'node:url'; import { existsSync, unlinkSync } from 'node:fs'; -const config = Object.freeze(JSON.parse(await readFile(new URL("./config.json", import.meta.url)))), { pages, text404 } = pkg; -const __dirname = path.resolve(); + +const config = Object.freeze(JSON.parse( + await readFile(new URL("./config.json", import.meta.url)) + )), + { pages, text404 } = pkg, + __dirname = path.resolve(); + +// Record the server's location as a URL object, including its host and port. const serverUrl = (base => { try { base = new URL(config.host); @@ -30,6 +36,8 @@ const serverUrl = (base => { })(); console.log(serverUrl); +// The server will check for the existence of this file when a shutdown is requested. +// The shutdown script in run-command.js will temporarily produce this file. const shutdown = fileURLToPath(new URL("./.shutdown", import.meta.url)); const rh = createRammerhead(); @@ -50,40 +58,36 @@ const rammerheadScopes = [ "/api/shuffleDict", "/mainport", ]; -const rammerheadSession = /^\/[a-z0-9]{32}/; -const shouldRouteRh = req => { - const url = new URL(req.url, serverUrl); - return ( - rammerheadScopes.includes(url.pathname) || - rammerheadSession.test(url.pathname) - ); -}; -const routeRhRequest = (req, res) => { - rh.emit("request", req, res); -}; -const routeRhUpgrade = (req, socket, head) => { - rh.emit("upgrade", req, socket, head); -}; + +const rammerheadSession = /^\/[a-z0-9]{32}/, + shouldRouteRh = req => { + const url = new URL(req.url, serverUrl); + return ( + rammerheadScopes.includes(url.pathname) || + rammerheadSession.test(url.pathname) + ); + }, + routeRhRequest = (req, res) => { + rh.emit("request", req, res); + }, + routeRhUpgrade = (req, socket, head) => { + rh.emit("upgrade", req, socket, head); + }; // Create a server factory for RH, and wisp (and bare if you please). const serverFactory = (handler) => { - return createServer() - .on('request', (req, res) => { - if (shouldRouteRh(req)) { - routeRhRequest(req, res); - } - else { - handler(req, res); - } - }) - .on('upgrade', (req, socket, head) => { - if (shouldRouteRh(req)) { - routeRhUpgrade(req, socket, head); - } - else if (req.url.endsWith('/wisp/')) { - wisp.routeRequest(req, socket, head); - } - }) + return createServer() + .on('request', (req, res) => { + if (shouldRouteRh(req)) + routeRhRequest(req, res); + else handler(req, res); + }) + .on('upgrade', (req, socket, head) => { + if (shouldRouteRh(req)) + routeRhUpgrade(req, socket, head); + else if (req.url.endsWith('/wisp/')) + wisp.routeRequest(req, socket, head); + }); }; // Set logger to true for logs @@ -91,73 +95,86 @@ const app = Fastify({ logger: false, serverFactory: serverFactory }); // Apply Helmet middleware for security app.register(fastifyHelmet, { - contentSecurityPolicy: false, // Disable CSP - xPoweredBy: false + contentSecurityPolicy: false, // Disable CSP + xPoweredBy: false +}); + +// Assign server file paths to different paths, for serving content on the website. +app.register(fastifyStatic, { + root: fileURLToPath(new URL("../views/pages", import.meta.url)), + decorateReply: false }); app.register(fastifyStatic, { - root: fileURLToPath(new URL("../views/pages", import.meta.url)), - decorateReply: false + root: fileURLToPath(new URL("../views/assets", import.meta.url)), + prefix: "/assets/", + decorateReply: false }); app.register(fastifyStatic, { - root: fileURLToPath(new URL("../views/assets", import.meta.url)), - prefix: "/assets/", - decorateReply: false + root: fileURLToPath(new URL("../views/archive", import.meta.url)), + prefix: "/arcade/", + decorateReply: false }); app.register(fastifyStatic, { - root: fileURLToPath(new URL("../views/archive", import.meta.url)), - prefix: "/arcade/", - decorateReply: false + root: fileURLToPath(new URL( +// Use the pre-compiled, minified scripts instead, if enabled in config. + config.minifyScripts ? "../views/dist/assets/js" : "../views/assets/js", + import.meta.url + )), + prefix: "/assets/js/", + decorateReply: false }); app.register(fastifyStatic, { - root: fileURLToPath(new URL( - config.minifyScripts ? "../views/dist/assets/js" : "../views/assets/js", - import.meta.url + root: fileURLToPath(new URL( +// Use the pre-compiled, minified stylesheets instead, if enabled in config. + config.minifyScripts ? "../views/dist/assets/css" : "../views/assets/css", + import.meta.url + )), + prefix: "/assets/css/", + decorateReply: false +}); + +// This combines scripts from the official UV repository with local UV scripts into +// one directory path. Local versions of files override the official versions. +app.register(fastifyStatic, { + root: [ + fileURLToPath(new URL( +// Use the pre-compiled, minified scripts instead, if enabled in config. + config.minifyScripts ? "../views/dist/uv" : "../views/uv", + import.meta.url )), - prefix: "/assets/js/", - decorateReply: false + uvPath + ], + prefix: "/uv/", + decorateReply: false +}); + +// Register proxy paths to the website. +app.register(fastifyStatic, { + root: epoxyPath, + prefix: "/epoxy/", + decorateReply: false }); app.register(fastifyStatic, { - root: fileURLToPath(new URL( - config.minifyScripts ? "../views/dist/assets/css" : "../views/assets/css", - import.meta.url - )), - prefix: "/assets/css/", - decorateReply: false + root: libcurlPath, + prefix: "/libcurl/", + decorateReply: false }); app.register(fastifyStatic, { - root: [fileURLToPath(new URL( - config.minifyScripts ? "../views/dist/uv" : "../views/uv", - import.meta.url - )), uvPath], - prefix: "/uv/", - decorateReply: false + root: bareModulePath, + prefix: "/bareasmodule/", + decorateReply: false }); app.register(fastifyStatic, { - root: epoxyPath, - prefix: "/epoxy/", - decorateReply: false -}); -app.register(fastifyStatic, { - root: libcurlPath, - prefix: "/libcurl/", - decorateReply: false -}); -app.register(fastifyStatic, { - root: bareModulePath, - prefix: "/bareasmodule/", - decorateReply: false -}); -app.register(fastifyStatic, { - root: baremuxPath, - prefix: "/baremux/", - decorateReply: false + root: baremuxPath, + prefix: "/baremux/", + decorateReply: false }); @@ -167,38 +184,41 @@ app.register(fastifyStatic, { // back here. Which path converts to what is defined in routes.mjs. app.get("/:file", (req, reply) => { - if (req.params.file === "test-shutdown" && existsSync(shutdown)) { - console.log("Holy Unblocker is shutting down."); - app.close(); - unlinkSync(shutdown); - process.exitCode = 0; - } +// If a GET request is sent to /test-shutdown and a script-generated shutdown file +// is present, gracefully shut the server down. + if (req.params.file === "test-shutdown" && existsSync(shutdown)) { + console.log("Holy Unblocker is shutting down."); + app.close(); + unlinkSync(shutdown); + process.exitCode = 0; + } -// Testing for future features that need cookies to deliver alternate source files. - if (req.raw.rawHeaders.includes("Cookie")) - console.log(req.raw.rawHeaders[req.raw.rawHeaders.indexOf("Cookie") + 1]); +// Testing for future features that need cookies to deliver alternate source files. + if (req.raw.rawHeaders.includes("Cookie")) + console.log(req.raw.rawHeaders[req.raw.rawHeaders.indexOf("Cookie") + 1]); - reply.type("text/html").send( - paintSource( - loadTemplates( - tryReadFile( - path.join(__dirname, - "views", -// Return the error page if the query is not found in -// routes.mjs. Also set index as the default page. - req.params.file - ? pages[req.params.file] || "error.html" - : pages.index - ) - ) - ) + reply.type("text/html").send( + paintSource( + loadTemplates( + tryReadFile( + path.join( + __dirname, + "views", +// Return the error page if the query is not found in routes.mjs. Also set +// the index the as the default page. + req.params.file + ? pages[req.params.file] || "error.html" + : pages.index + ) ) - ); + ) + ) + ); }); // Ignore trailing slashes for the above path handling. app.get("/:file/", (req, reply) => { - reply.redirect("/" + req.params.file); + reply.redirect("/" + req.params.file); }); @@ -206,15 +226,15 @@ app.get("/:file/", (req, reply) => { Testing for future restructuring of this config file. app.get("/assets/js/uv/uv.config.js", (req, reply) => { - console.log(req.url); - reply.type("text/javascript"); - reply.send(tryReadFile(path.join(__dirname, "views/assets/js/uv/uv.config.js"))); + console.log(req.url); + reply.type("text/javascript"); + reply.send(tryReadFile(path.join(__dirname, "views/assets/js/uv/uv.config.js"))); }); */ // Set an error page for invalid paths outside the query string system. app.setNotFoundHandler((req, reply) => { - reply.code(404).type("text/html").send(paintSource(loadTemplates(tryReadFile(path.join(__dirname, "views/error.html"))))); + reply.code(404).type("text/html").send(paintSource(loadTemplates(tryReadFile(path.join(__dirname, "views/error.html"))))); }); // Configure host to your liking, but remember to tweak the Rammerhead IP