import Fastify from 'fastify'; import { createServer } from 'node:http'; import wisp from 'wisp-server-node'; import createRammerhead from "../lib/rammerhead/src/server/index.js"; import { epoxyPath } from "@mercuryworkshop/epoxy-transport"; import { libcurlPath } from "@mercuryworkshop/libcurl-transport"; import { bareModulePath } from "@mercuryworkshop/bare-as-module3"; import { baremuxPath } from "@mercuryworkshop/bare-mux/node"; import { uvPath } from "@titaniumnetwork-dev/ultraviolet"; import fastifyHelmet from '@fastify/helmet'; import fastifyStatic from '@fastify/static'; import pageRoutes from "./routes.mjs"; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { paintSource, preloaded404, tryReadFile } from './randomization.mjs'; import loadTemplates from './templates.mjs'; import { fileURLToPath } from 'node:url'; import { existsSync, unlinkSync } from 'node:fs'; import ecosystem from '../ecosystem.config.js'; const config = Object.freeze( JSON.parse(await readFile(new URL("./config.json", import.meta.url))) ), ecosystemConfig = Object.freeze( ecosystem.apps.find(app => app.name === "HolyUB") || ecosystem.apps[0] ), { pages, externalPages } = pageRoutes, __dirname = path.resolve(); // Record the server's location as a URL object, including its host and port. // The host can be modified at /src/config.json, whereas the ports can be modified // at /ecosystem.config.js. const serverUrl = (base => { try { base = new URL(config.host); } catch (e) { base = new URL("http://a"); base.host = config.host; } base.port = ecosystemConfig[ config.production ? "env_production" : "env" ].PORT; return Object.freeze(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(); const rammerheadScopes = [ "/rammerhead.js", "/hammerhead.js", "/transport-worker.js", "/task.js", "/iframe-task.js", "/worker-hammerhead.js", "/messaging", "/sessionexists", "/deletesession", "/newsession", "/editsession", "/needpassword", "/syncLocalStorage", "/api/shuffleDict", "/mainport", ]; const rammerheadSession = /^\/[a-z0-9]{32}/, shouldRouteRh = req => { try { const url = new URL(req.url, serverUrl); return ( rammerheadScopes.includes(url.pathname) || rammerheadSession.test(url.pathname) ); } catch (e) {return false} }, 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); }); }; // Set logger to true for logs const app = Fastify({ ignoreDuplicateSlashes: true, ignoreTrailingSlash: true, logger: false, serverFactory: serverFactory }); // Apply Helmet middleware for security app.register(fastifyHelmet, { 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/assets", import.meta.url)), prefix: "/assets/", decorateReply: false }); app.register(fastifyStatic, { root: fileURLToPath(new URL("../views/archive", import.meta.url)), prefix: "/arcade/", decorateReply: false }); app.register(fastifyStatic, { 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( // 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 )), uvPath ], prefix: "/uv/", decorateReply: false }); // Register proxy paths to the website. 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 }); // All website files are stored in the /views directory. // This takes one of those files and displays it for a site visitor. // Paths like /browsing are converted into paths like /views/pages/surf.html // back here. Which path converts to what is defined in routes.mjs. app.get("/:path", (req, reply) => { // 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 ]); const reqPath = req.params.path; if (reqPath in externalPages) { let externalRoute = externalPages[reqPath]; if (typeof externalRoute !== "string") externalRoute = externalRoute.default; return reply.redirect(externalRoute); } // If a GET request is sent to /test-shutdown and a script-generated shutdown file // is present, gracefully shut the server down. if (reqPath === "test-shutdown" && existsSync(shutdown)) { console.log("Holy Unblocker is shutting down."); app.close(); unlinkSync(shutdown); process.exitCode = 0; } // Return the error page if the query is not found in routes.mjs. if (reqPath && !(reqPath in pages)) return reply.code(404).type("text/html").send(preloaded404); reply.type("text/html").send( paintSource( loadTemplates( tryReadFile( path.join( __dirname, "views", // Set the index the as the default page. reqPath ? pages[reqPath] : pages.index ) ) ) ) ); }); app.get("/github/:redirect", (req, reply) => { if (req.params.redirect in externalPages.github) reply.redirect(externalPages.github[req.params.redirect]); else reply.code(404).type("text/html").send(preloaded404); }); /* 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"))); }); */ // Set an error page for invalid paths outside the query string system. app.setNotFoundHandler((req, reply) => { reply.code(404).type("text/html").send(preloaded404); }); app.listen({ port: serverUrl.port, host: serverUrl.hostname }); console.log(`Holy Unblocker is listening on port ${serverUrl.port}.`);