diff --git a/esbuild.config.js b/esbuild.config.js
index b0e0f7a..1b82481 100644
--- a/esbuild.config.js
+++ b/esbuild.config.js
@@ -119,7 +119,7 @@ esbuild.build({
bundle: true,
outfile: "dist/svr.js", // Output file
platform: "node",
- target: "es2020",
+ target: "es2017",
plugins: [
esbuildCopyPlugin.copy({
resolveFrom: __dirname,
diff --git a/eslint.config.mjs b/eslint.config.mjs
index fd22806..26fd9c1 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -26,5 +26,5 @@ export default [
}
},
pluginJs.configs.recommended,
- eslintPluginPrettierRecommended,
+ eslintPluginPrettierRecommended
];
diff --git a/src/index.js b/src/index.js
index 763b09a..20f7d69 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,4 +1,244 @@
-var http = require("http");
-http.createServer((req, res) => {
- res.end("Hello World!");
-}).listen(3000);
+const http = require("http");
+const fs = require("fs");
+const cluster = require("./utils/clusterBunShim.js"); // Cluster module with shim for Bun
+const sanitizeURL = require("./utils/urlSanitizer.js");
+const generateErrorStack = require("./utils/generateErrorStack.js");
+const serverHTTPErrorDescs = require("./res/httpErrorDescriptions.js");
+const getOS = require("./utils/getOS.js");
+const svrjsInfo = require("../svrjs.json");
+const version = svrjsInfo.version;
+//const parseURL = require("./utils/urlParser.js");
+//const fixNodeMojibakeURL = require("./utils/urlMojibakeFixer.js");
+
+// Create log, mods and temp directories, if they don't exist.
+if (!fs.existsSync(__dirname + "/log")) fs.mkdirSync(__dirname + "/log");
+if (!fs.existsSync(__dirname + "/mods")) fs.mkdirSync(__dirname + "/mods");
+if (!fs.existsSync(__dirname + "/temp")) fs.mkdirSync(__dirname + "/temp");
+
+const serverconsoleConstructor = require("./utils/serverconsole.js");
+
+let config = {};
+
+// TODO: configuration from config.json
+let page404 = "404.html"
+let errorPages = [];
+let stackHidden = true;
+let exposeServerVersion = false;
+let exposeModsInErrorPages = false;
+let enableLogging = true;
+let serverAdmin = "webmaster@svrjs.org";
+function getCustomHeaders() {
+ return {};
+}
+
+const serverconsole = serverconsoleConstructor(enableLogging);
+
+function requestHandler(req, res) {
+ // TODO: variables from SVR.JS 3.x
+ let isProxy = false;
+
+ let reqIdInt = Math.floor(Math.random() * 16777216);
+ if (reqIdInt == 16777216) reqIdInt = 0;
+ const reqId = "0".repeat(6 - reqIdInt.toString(16).length) + reqIdInt.toString(16);
+
+ // SVR.JS log facilities
+ const logFacilities = {
+ climessage: (msg) => serverconsole.climessage(msg, reqId),
+ reqmessage: (msg) => serverconsole.reqmessage(msg, reqId),
+ resmessage: (msg) => serverconsole.resmessage(msg, reqId),
+ errmessage: (msg) => serverconsole.errmessage(msg, reqId),
+ locerrmessage: (msg) => serverconsole.locerrmessage(msg, reqId),
+ locwarnmessage: (msg) => serverconsole.locwarnmessage(msg, reqId),
+ locmessage: (msg) => serverconsole.locmessage(msg, reqId)
+ };
+
+ // Server error calling method
+ res.error = (errorCode, extName, stack, ch) => {
+ if (typeof errorCode !== "number") {
+ throw new TypeError("HTTP error code parameter needs to be an integer.");
+ }
+
+ // Handle optional parameters
+ if (extName && typeof extName === "object") {
+ ch = stack;
+ stack = extName;
+ extName = undefined;
+ } else if (typeof extName !== "string" && extName !== null && extName !== undefined) {
+ throw new TypeError("Extension name parameter needs to be a string.");
+ }
+
+ if (stack && typeof stack === "object" && Object.prototype.toString.call(stack) !== "[object Error]") {
+ ch = stack;
+ stack = undefined;
+ } else if (typeof stack !== "object" && typeof stack !== "string" && stack) {
+ throw new TypeError("Error stack parameter needs to be either a string or an instance of Error object.");
+ }
+
+ // Determine error file
+ function getErrorFileName(list, callback, _i) {
+
+ function medCallback(p) {
+ if (p) callback(p);
+ else {
+ if (errorCode == 404) {
+ fs.access(page404, fs.constants.F_OK, function (err) {
+ if (err) {
+ fs.access("." + errorCode.toString(), fs.constants.F_OK, function (err) {
+ try {
+ if (err) {
+ callback(errorCode.toString() + ".html");
+ } else {
+ callback("." + errorCode.toString());
+ }
+ } catch (err2) {
+ res.error(500, err2);
+ }
+ });
+ } else {
+ try {
+ callback(page404);
+ } catch (err2) {
+ res.error(500, err2);
+ }
+ }
+ });
+ } else {
+ fs.access("." + errorCode.toString(), fs.constants.F_OK, function (err) {
+ try {
+ if (err) {
+ callback(errorCode.toString() + ".html");
+ } else {
+ callback("." + errorCode.toString());
+ }
+ } catch (err2) {
+ res.error(500, err2);
+ }
+ });
+ }
+ }
+ }
+
+ if (!_i) _i = 0;
+ if (_i >= list.length) {
+ medCallback(false);
+ return;
+ }
+
+ if (list[_i].scode != errorCode || !(matchHostname(list[_i].host) && ipMatch(list[_i].ip, req.socket ? req.socket.localAddress : undefined))) {
+ getErrorFileName(list, callback, _i + 1);
+ return;
+ } else {
+ fs.access(list[_i].path, fs.constants.F_OK, function (err) {
+ if (err) {
+ getErrorFileName(list, callback, _i + 1);
+ } else {
+ medCallback(list[_i].path);
+ }
+ });
+ }
+ }
+
+ getErrorFileName(errorPages, function (errorFile) {
+ // Generate error stack if not provided
+ if (Object.prototype.toString.call(stack) === "[object Error]") stack = generateErrorStack(stack);
+ if (stack === undefined) stack = generateErrorStack(new Error("Unknown error"));
+
+ if (errorCode == 500 || errorCode == 502) {
+ //TODO: server logging facilities
+ logFacilities.errmessage("There was an error while processing the request!");
+ logFacilities.errmessage("Stack:");
+ logFacilities.errmessage(stack);
+ }
+
+ // Hide the error stack if specified
+ if (stackHidden) stack = "[error stack hidden]";
+
+ // Validate the error code and handle unknown codes
+ if (serverHTTPErrorDescs[errorCode] === undefined) {
+ res.error(501, extName, stack);
+ } else {
+ // Process custom headers if provided
+ let cheaders = { ...getCustomHeaders(), ...ch };
+
+ cheaders["Content-Type"] = "text/html; charset=utf-8";
+
+ // Set default Allow header for 405 error if not provided
+ if (errorCode == 405 && !cheaders["Allow"]) cheaders["Allow"] = "GET, POST, HEAD, OPTIONS";
+
+ // Read the error file and replace placeholders with error information
+ fs.readFile(errorFile, function (err, data) {
+ try {
+ if (err) throw err;
+ res.writeHead(errorCode, http.STATUS_CODES[errorCode], cheaders);
+ responseEnd(data.toString().replace(/{errorMessage}/g, errorCode.toString() + " " + http.STATUS_CODES[errorCode].replace(/&/g, "&").replace(//g, ">")).replace(/{errorDesc}/g, serverHTTPErrorDescs[errorCode]).replace(/{stack}/g, stack.replace(/&/g, "&").replace(//g, ">").replace(/\r\n/g, "
").replace(/\n/g, "
").replace(/\r/g, "
").replace(/ {2}/g, " ")).replace(/{path}/g, req.url.replace(/&/g, "&").replace(//g, ">")).replace(/{server}/g, "" + ((exposeServerVersion ? "SVR.JS/" + version + " (" + getOS() + "; " + (process.isBun ? ("Bun/v" + process.versions.bun + "; like Node.JS/" + process.version) : ("Node.JS/" + process.version)) + ")" : "SVR.JS") + ((!exposeModsInErrorPages || extName == undefined) ? "" : " " + extName)).replace(/&/g, "&").replace(//g, ">") + ((req.headers.host == undefined || isProxy) ? "" : " on " + String(req.headers.host).replace(/&/g, "&").replace(//g, ">"))).replace(/{contact}/g, serverAdmin.replace(/&/g, "&").replace(//g, ">").replace(/\./g, "[dot]").replace(/@/g, "[at]"))); // Replace placeholders in error response
+ } catch (err) {
+ let additionalError = 500;
+ // Handle additional error cases
+ if (err.code == "ENOENT") {
+ additionalError = 404;
+ } else if (err.code == "ENOTDIR") {
+ additionalError = 404; // Assume that file doesn't exist
+ } else if (err.code == "EACCES") {
+ additionalError = 403;
+ } else if (err.code == "ENAMETOOLONG") {
+ additionalError = 414;
+ } else if (err.code == "EMFILE") {
+ additionalError = 503;
+ } else if (err.code == "ELOOP") {
+ additionalError = 508;
+ }
+
+ res.writeHead(errorCode, http.STATUS_CODES[errorCode], cheaders);
+ res.write(("
{errorMessage}{errorMessage}
{errorDesc}
" + ((additionalError == 404) ? "" : "Additionally, a {additionalError} error occurred while loading an error page.
") + "{server}
").replace(/{errorMessage}/g, errorCode.toString() + " " + http.STATUS_CODES[errorCode].replace(/&/g, "&").replace(//g, ">")).replace(/{errorDesc}/g, serverHTTPErrorDescs[errorCode]).replace(/{stack}/g, stack.replace(/&/g, "&").replace(//g, ">").replace(/\r\n/g, "
").replace(/\n/g, "
").replace(/\r/g, "
").replace(/ {2}/g, " ")).replace(/{path}/g, req.url.replace(/&/g, "&").replace(//g, ">")).replace(/{server}/g, "" + ((exposeServerVersion ? "SVR.JS/" + version + " (" + getOS() + "; " + (process.isBun ? ("Bun/v" + process.versions.bun + "; like Node.JS/" + process.version) : ("Node.JS/" + process.version)) + ")" : "SVR.JS") + ((!exposeModsInErrorPages || extName == undefined) ? "" : " " + extName)).replace(/&/g, "&").replace(//g, ">") + ((req.headers.host == undefined || isProxy) ? "" : " on " + String(req.headers.host).replace(/&/g, "&").replace(//g, ">"))).replace(/{contact}/g, serverAdmin.replace(/&/g, "&").replace(//g, ">").replace(/\./g, "[dot]").replace(/@/g, "[at]")).replace(/{additionalError}/g, additionalError.toString())); // Replace placeholders in error response
+ res.end();
+ }
+ });
+ }
+ });
+ }
+
+ // Function to perform HTTP redirection to a specified destination URL
+ res.redirect = (destination, isTemporary, keepMethod, customHeaders) => {
+ // If keepMethod is a object, then save it to customHeaders
+ if (typeof keepMethod == "object") customHeaders = keepMethod;
+
+ // If isTemporary is a object, then save it to customHeaders
+ if (typeof isTemporary == "object") customHeaders = isTemporary;
+
+ // If customHeaders are not provided, get the default custom headers
+ if (customHeaders === undefined) customHeaders = getCustomHeaders();
+
+ // Set the "Location" header to the destination URL
+ customHeaders["Location"] = destination;
+
+ // Determine the status code for redirection based on the isTemporary and keepMethod flags
+ const statusCode = keepMethod ? (isTemporary ? 307 : 308) : (isTemporary ? 302 : 301);
+
+ // Write the response header with the appropriate status code and message
+ res.writeHead(statusCode, http.STATUS_CODES[statusCode], customHeaders);
+
+ // Log the redirection message
+ logFacilities("Client redirected to " + destination);
+
+ // End the response
+ res.end();
+
+ // Return from the function
+ return;
+ }
+
+ try {
+ req.parsedURL = new URL(req.url, "http" + (req.socket.encrypted ? "s" : "") + "://" + (req.headers.host ? req.headers.host : (domain ? domain : "unknown.invalid")));
+ } catch (err) {
+ res.error(400, err);
+ return;
+ }
+
+ //logFacilities.resmessage("svrjs");
+
+ // Call res.error (equivalent to callServerError in SVR.JS 3.x)
+ res.error(200);
+}
+
+// Create HTTP server
+http.createServer(requestHandler).listen(3000);
diff --git a/src/res/httpErrorDescriptions.js b/src/res/httpErrorDescriptions.js
new file mode 100644
index 0000000..7625909
--- /dev/null
+++ b/src/res/httpErrorDescriptions.js
@@ -0,0 +1,52 @@
+// HTTP error descriptions
+var serverHTTPErrorDescs = {
+ 200: "The request succeeded! :)",
+ 201: "A new resource has been created.",
+ 202: "The request has been accepted for processing, but the processing has not been completed.",
+ 400: "The request you made is invalid.",
+ 401: "You need to authenticate yourself in order to access the requested file.",
+ 402: "You need to pay in order to access the requested file.",
+ 403: "You don't have access to the requested file.",
+ 404: "The requested file doesn't exist. If you have typed the URL manually, then please check the spelling.",
+ 405: "Method used to access the requested file isn't allowed.",
+ 406: "The request is capable of generating only unacceptable content.",
+ 407: "You need to authenticate yourself in order to use the proxy.",
+ 408: "You have timed out.",
+ 409: "The request you sent conflicts with the current state of the server.",
+ 410: "The requested file is permanently deleted.",
+ 411: "Content-Length property is required.",
+ 412: "The server doesn't meet the preconditions you put in the request.",
+ 413: "The request you sent is too large.",
+ 414: "The URL you sent is too long.",
+ 415: "The media type of request you sent isn't supported by the server.",
+ 416: "The requested content range (Content-Range header) you sent is unsatisfiable.",
+ 417: "The expectation specified in the Expect property couldn't be satisfied.",
+ 418: "The server (teapot) can't brew any coffee! ;)",
+ 421: "The request you made isn't intended for this server.",
+ 422: "The server couldn't process content sent by you.",
+ 423: "The requested file is locked.",
+ 424: "The request depends on another failed request.",
+ 425: "The server is unwilling to risk processing a request that might be replayed.",
+ 426: "You need to upgrade the protocols you use to request a file.",
+ 428: "The request you sent needs to be conditional, but it isn't.",
+ 429: "You sent too many requests to the server.",
+ 431: "The request you sent contains headers that are too large.",
+ 451: "The requested file isn't accessible for legal reasons.",
+ 497: "You sent a non-TLS request to the HTTPS server.",
+ 500: "The server had an unexpected error. Below, the error stack is shown: {stack}
You may need to contact the server administrator at {contact}.",
+ 501: "The request requires the use of a function, which isn't currently implemented by the server.",
+ 502: "The server had an error while it was acting as a gateway.
You may need to contact the server administrator at {contact}.",
+ 503: "The service provided by the server is currently unavailable, possibly due to maintenance downtime or capacity problems. Please try again later.
You may need to contact the server administrator at {contact}.",
+ 504: "The server couldn't get a response in time while it was acting as a gateway.
You may need to contact the server administrator at {contact}.",
+ 505: "The server doesn't support the HTTP version used in the request.",
+ 506: "The Variant header is configured to be engaged in content negotiation.
You may need to contact the server administrator at {contact}.",
+ 507: "The server ran out of disk space necessary to complete the request.",
+ 508: "The server detected an infinite loop while processing the request.",
+ 509: "The server has its bandwidth limit exceeded.
You may need to contact the server administrator at {contact}.",
+ 510: "The server requires an extended HTTP request. The request you made isn't an extended HTTP request.",
+ 511: "You need to authenticate yourself in order to get network access.",
+ 598: "The server couldn't get a response in time while it was acting as a proxy.",
+ 599: "The server couldn't connect in time while it was acting as a proxy.",
+};
+
+module.exports = serverHTTPErrorDescs;
diff --git a/src/utils/clusterBunShim.js b/src/utils/clusterBunShim.js
new file mode 100644
index 0000000..1c6e34e
--- /dev/null
+++ b/src/utils/clusterBunShim.js
@@ -0,0 +1,230 @@
+const net = require("net");
+const os = require("os");
+const path = require("path");
+
+let cluster = {};
+try {
+ // Import cluster module
+ cluster = require("cluster");
+} catch (err) {
+ // Clustering is not supported!
+}
+
+// Cluster & IPC shim for Bun
+
+cluster.bunShim = function () {
+ cluster.isMaster = !process.env.NODE_UNIQUE_ID;
+ cluster.isPrimary = cluster.isMaster;
+ cluster.isWorker = !cluster.isMaster;
+ cluster.__shimmed__ = true;
+
+ if (cluster.isWorker) {
+ // Shim the cluster.worker object for worker processes
+ cluster.worker = {
+ id: parseInt(process.env.NODE_UNIQUE_ID),
+ process: process,
+ isDead: function () {
+ return false;
+ },
+ send: function (message, b, c, d) {
+ process.send(message, b, c, d);
+ },
+ };
+
+ if (!process.send) {
+ // Shim the process.send function for worker processes
+
+ // Create a fake IPC server to receive messages
+ let fakeIPCServer = net.createServer(function (socket) {
+ let receivedData = "";
+
+ socket.on("data", function (data) {
+ receivedData += data.toString();
+ });
+
+ socket.on("end", function () {
+ process.emit("message", receivedData);
+ });
+ });
+ fakeIPCServer.listen(
+ os.platform() === "win32"
+ ? path.join(
+ "\\\\?\\pipe",
+ __dirname,
+ "temp/.W" + process.pid + ".ipc",
+ )
+ : __dirname + "/temp/.W" + process.pid + ".ipc",
+ );
+
+ process.send = function (message) {
+ // Create a fake IPC connection to send messages
+ let fakeIPCConnection = net.createConnection(
+ os.platform() === "win32"
+ ? path.join(
+ "\\\\?\\pipe",
+ __dirname,
+ "temp/.P" + process.pid + ".ipc",
+ )
+ : __dirname + "/temp/.P" + process.pid + ".ipc",
+ function () {
+ fakeIPCConnection.end(message);
+ },
+ );
+ };
+
+ process.removeFakeIPC = function () {
+ // Close IPC server
+ process.send = function () {};
+ fakeIPCServer.close();
+ };
+ }
+ }
+
+ // Custom implementation for cluster.fork()
+ cluster._workersCounter = 1;
+ cluster.workers = {};
+ cluster.fork = function (env) {
+ const child_process = require("child_process");
+ let newEnvironment = JSON.parse(JSON.stringify(env ? env : process.env));
+ newEnvironment.NODE_UNIQUE_ID = cluster._workersCounter;
+ let newArguments = JSON.parse(JSON.stringify(process.argv));
+ let command = newArguments.shift();
+ let newWorker = child_process.spawn(command, newArguments, {
+ env: newEnvironment,
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
+ });
+
+ newWorker.process = newWorker;
+ newWorker.isDead = function () {
+ return newWorker.exitCode !== null || newWorker.killed;
+ };
+ newWorker.id = newEnvironment.NODE_UNIQUE_ID;
+
+ function checkSendImplementation(worker) {
+ let sendImplemented = true;
+
+ if (
+ !(
+ process.versions &&
+ process.versions.bun &&
+ process.versions.bun[0] != "0"
+ )
+ ) {
+ if (!worker.send) {
+ sendImplemented = false;
+ }
+
+ let oldLog = console.log;
+ console.log = function (a, b, c, d, e, f) {
+ if (
+ a == "ChildProcess.prototype.send() - Sorry! Not implemented yet"
+ ) {
+ throw new Error("NOT IMPLEMENTED");
+ } else {
+ oldLog(a, b, c, d, e, f);
+ }
+ };
+
+ try {
+ worker.send(undefined);
+ } catch (err) {
+ if (err.message === "NOT IMPLEMENTED") {
+ sendImplemented = false;
+ }
+ console.log(err);
+ }
+
+ console.log = oldLog;
+ }
+
+ return sendImplemented;
+ }
+
+ if (!checkSendImplementation(newWorker)) {
+ // Create a fake IPC server for worker process to receive messages
+ let fakeWorkerIPCServer = net.createServer(function (socket) {
+ let receivedData = "";
+
+ socket.on("data", function (data) {
+ receivedData += data.toString();
+ });
+
+ socket.on("end", function () {
+ newWorker.emit("message", receivedData);
+ });
+ });
+ fakeWorkerIPCServer.listen(
+ os.platform() === "win32"
+ ? path.join(
+ "\\\\?\\pipe",
+ __dirname,
+ "temp/.P" + newWorker.process.pid + ".ipc",
+ )
+ : __dirname + "/temp/.P" + newWorker.process.pid + ".ipc",
+ );
+
+ // Cleanup when worker process exits
+ newWorker.on("exit", function () {
+ fakeWorkerIPCServer.close();
+ delete cluster.workers[newWorker.id];
+ });
+
+ newWorker.send = function (
+ message,
+ fakeParam2,
+ fakeParam3,
+ fakeParam4,
+ tries,
+ ) {
+ if (!tries) tries = 0;
+
+ try {
+ // Create a fake IPC connection to send messages to worker process
+ let fakeWorkerIPCConnection = net.createConnection(
+ os.platform() === "win32"
+ ? path.join(
+ "\\\\?\\pipe",
+ __dirname,
+ "temp/.W" + newWorker.process.pid + ".ipc",
+ )
+ : __dirname + "/temp/.W" + newWorker.process.pid + ".ipc",
+ function () {
+ fakeWorkerIPCConnection.end(message);
+ },
+ );
+ } catch (err) {
+ if (tries > 50) throw err;
+ newWorker.send(
+ message,
+ fakeParam2,
+ fakeParam3,
+ fakeParam4,
+ tries + 1,
+ );
+ }
+ };
+ } else {
+ newWorker.on("exit", function () {
+ delete cluster.workers[newWorker.id];
+ });
+ }
+
+ cluster.workers[newWorker.id] = newWorker;
+ cluster._workersCounter++;
+ return newWorker;
+ };
+};
+
+if (
+ process.isBun &&
+ (cluster.isMaster === undefined ||
+ (cluster.isMaster && process.env.NODE_UNIQUE_ID))
+) {
+ cluster.bunShim();
+}
+
+// Shim cluster.isPrimary field
+if (cluster.isPrimary === undefined && cluster.isMaster !== undefined)
+ cluster.isPrimary = cluster.isMaster;
+
+module.exports = cluster;
diff --git a/src/utils/generateErrorStack.js b/src/utils/generateErrorStack.js
new file mode 100644
index 0000000..d1f23ff
--- /dev/null
+++ b/src/utils/generateErrorStack.js
@@ -0,0 +1,50 @@
+// Generate V8-style error stack from Error object.
+function generateErrorStack(errorObject) {
+ // Split the error stack by newlines.
+ var errorStack = errorObject.stack ? errorObject.stack.split("\n") : [];
+
+ // If the error stack starts with the error name, return the original stack (it is V8-style then).
+ if (
+ errorStack.some(function (errorStackLine) {
+ return errorStackLine.indexOf(errorObject.name) == 0;
+ })
+ ) {
+ return errorObject.stack;
+ }
+
+ // Create a new error stack with the error name and code (if available).
+ var newErrorStack = [
+ errorObject.name +
+ (errorObject.code ? ": " + errorObject.code : "") +
+ (errorObject.message == "" ? "" : ": " + errorObject.message),
+ ];
+
+ // Process each line of the original error stack.
+ errorStack.forEach(function (errorStackLine) {
+ if (errorStackLine != "") {
+ // Split the line into function and location parts (if available).
+ var errorFrame = errorStackLine.split("@");
+ var location = "";
+ if (errorFrame.length > 1 && errorFrame[0] == "global code")
+ errorFrame.shift();
+ if (errorFrame.length > 1) location = errorFrame.pop();
+ var func = errorFrame.join("@");
+
+ // Build the new error stack entry with function and location information.
+ newErrorStack.push(
+ " at " +
+ (func == ""
+ ? !location || location == ""
+ ? ""
+ : location
+ : func +
+ (!location || location == "" ? "" : " (" + location + ")")),
+ );
+ }
+ });
+
+ // Join the new error stack entries with newlines and return the final stack.
+ return newErrorStack.join("\n");
+}
+
+module.exports = generateErrorStack;
diff --git a/src/utils/getOS.js b/src/utils/getOS.js
new file mode 100644
index 0000000..7f181eb
--- /dev/null
+++ b/src/utils/getOS.js
@@ -0,0 +1,31 @@
+const os = require("os");
+
+function getOS() {
+ var osType = os.type();
+ var platform = os.platform();
+ if (platform == "android") {
+ return "Android";
+ } else if (osType == "Windows_NT" || osType == "WindowsNT") {
+ var arch = os.arch();
+ if (arch == "ia32") {
+ return "Win32";
+ } else if (arch == "x64") {
+ return "Win64";
+ } else {
+ return "Win" + arch.toUpperCase();
+ }
+ } else if (osType.indexOf("CYGWIN") == 0) {
+ return "Cygwin";
+ } else if (osType.indexOf("MINGW") == 0) {
+ return "MinGW";
+ } else if (osType.indexOf("MSYS") == 0) {
+ return "MSYS";
+ } else if (osType.indexOf("UWIN") == 0) {
+ return "UWIN";
+ } else if (osType == "GNU") {
+ return "GNU Hurd";
+ } else {
+ return osType;
+ }
+}
+module.exports = getOS;
diff --git a/src/utils/helper.js b/src/utils/helper.js
deleted file mode 100644
index 775887f..0000000
--- a/src/utils/helper.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// src/utils/helper.js
-function add(a, b) {
- return a + b;
-}
-
-module.exports = {
- add,
-};
diff --git a/src/utils/serverconsole.js b/src/utils/serverconsole.js
new file mode 100644
index 0000000..e7f588d
--- /dev/null
+++ b/src/utils/serverconsole.js
@@ -0,0 +1,277 @@
+const fs = require("fs");
+
+let enableLoggingIntoFile = false;
+let logFile = undefined;
+let logSync = false;
+let cluster = require("./clusterBunShim.js");
+let reallyExiting = false;
+let timestamp = new Date().getTime();
+
+// Logging function
+function LOG(s) {
+ try {
+ if (enableLoggingIntoFile) {
+ if (logSync) {
+ fs.appendFileSync(
+ __dirname +
+ "/log/" +
+ (cluster.isPrimary
+ ? "master"
+ : cluster.isPrimary === undefined
+ ? "singlethread"
+ : "worker") +
+ "-" +
+ timestamp +
+ ".log",
+ "[" + new Date().toISOString() + "] " + s + "\r\n",
+ );
+ } else {
+ if (!logFile) {
+ logFile = fs.createWriteStream(
+ __dirname +
+ "/log/" +
+ (cluster.isPrimary
+ ? "master"
+ : cluster.isPrimary === undefined
+ ? "singlethread"
+ : "worker") +
+ "-" +
+ timestamp +
+ ".log",
+ {
+ flags: "a",
+ autoClose: false,
+ },
+ );
+ logFile.on("error", function (err) {
+ if (
+ !s.match(
+ /^SERVER WARNING MESSAGE(?: \[Request Id: [0-9a-f]{6}\])?: There was a problem while saving logs! Logs will not be kept in log file\. Reason: /,
+ ) &&
+ !reallyExiting
+ )
+ serverconsole.locwarnmessage(
+ "There was a problem while saving logs! Logs will not be kept in log file. Reason: " +
+ err.message,
+ );
+ });
+ }
+ if (logFile.writable) {
+ logFile.write("[" + new Date().toISOString() + "] " + s + "\r\n");
+ } else {
+ throw new Error("Log file stream is closed.");
+ }
+ }
+ }
+ } catch (err) {
+ if (
+ !s.match(
+ /^SERVER WARNING MESSAGE(?: \[Request Id: [0-9a-f]{6}\])?: There was a problem while saving logs! Logs will not be kept in log file\. Reason: /,
+ ) &&
+ !reallyExiting
+ )
+ serverconsole.locwarnmessage(
+ "There was a problem while saving logs! Logs will not be kept in log file. Reason: " +
+ err.message,
+ );
+ }
+}
+
+// Server console function
+var serverconsole = {
+ climessage: (msg, reqId) => {
+ if (msg.indexOf("\n") != -1) {
+ msg.split("\n").forEach((nmsg) => {
+ serverconsole.climessage(nmsg);
+ });
+ return;
+ }
+ console.log(
+ "\x1b[1mSERVER CLI MESSAGE\x1b[22m" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg,
+ );
+ LOG(
+ "SERVER CLI MESSAGE" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg,
+ );
+ return;
+ },
+ reqmessage: (msg, reqId) => {
+ if (msg.indexOf("\n") != -1) {
+ msg.split("\n").forEach((nmsg) => {
+ serverconsole.reqmessage(nmsg);
+ });
+ return;
+ }
+ console.log(
+ "\x1b[34m\x1b[1mSERVER REQUEST MESSAGE\x1b[22m" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg +
+ "\x1b[37m\x1b[0m",
+ );
+ LOG(
+ "SERVER REQUEST MESSAGE" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg,
+ );
+ return;
+ },
+ resmessage: (msg, reqId) => {
+ if (msg.indexOf("\n") != -1) {
+ msg.split("\n").forEach((nmsg) => {
+ serverconsole.resmessage(nmsg);
+ });
+ return;
+ }
+ console.log(
+ "\x1b[32m\x1b[1mSERVER RESPONSE MESSAGE\x1b[22m" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg +
+ "\x1b[37m\x1b[0m",
+ );
+ LOG(
+ "SERVER RESPONSE MESSAGE" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg,
+ );
+ return;
+ },
+ errmessage: (msg, reqId) => {
+ if (msg.indexOf("\n") != -1) {
+ msg.split("\n").forEach((nmsg) => {
+ serverconsole.errmessage(nmsg);
+ });
+ return;
+ }
+ console.log(
+ "\x1b[31m\x1b[1mSERVER RESPONSE ERROR MESSAGE\x1b[22m" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg +
+ "\x1b[37m\x1b[0m",
+ );
+ LOG(
+ "SERVER RESPONSE ERROR MESSAGE" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg,
+ );
+ return;
+ },
+ locerrmessage: (msg, reqId) => {
+ if (msg.indexOf("\n") != -1) {
+ msg.split("\n").forEach((nmsg) => {
+ serverconsole.locerrmessage(nmsg);
+ });
+ return;
+ }
+ console.log(
+ "\x1b[41m\x1b[1mSERVER ERROR MESSAGE\x1b[22m" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg +
+ "\x1b[40m\x1b[0m",
+ );
+ LOG(
+ "SERVER ERROR MESSAGE" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg,
+ );
+ return;
+ },
+ locwarnmessage: (msg, reqId) => {
+ if (msg.indexOf("\n") != -1) {
+ msg.split("\n").forEach((nmsg) => {
+ serverconsole.locwarnmessage(nmsg);
+ });
+ return;
+ }
+ console.log(
+ "\x1b[43m\x1b[1mSERVER WARNING MESSAGE\x1b[22m" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg +
+ "\x1b[40m\x1b[0m",
+ );
+ LOG(
+ "SERVER WARNING MESSAGE" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg,
+ );
+ return;
+ },
+ locmessage: (msg, reqId) => {
+ if (msg.indexOf("\n") != -1) {
+ msg.split("\n").forEach((nmsg) => {
+ serverconsole.locmessage(nmsg);
+ });
+ return;
+ }
+ console.log(
+ "\x1b[1mSERVER MESSAGE\x1b[22m" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg,
+ );
+ LOG(
+ "SERVER MESSAGE" +
+ (reqId ? " [Request Id: " + reqId + "]" : "") +
+ ": " +
+ msg,
+ );
+ return;
+ },
+ setProcessExiting: (state) => {
+ reallyExiting = state;
+ },
+};
+
+// Wrap around process.exit, so that log contents can be flushed.
+process.unsafeExit = process.exit;
+process.exit = function (code) {
+ if (logFile && logFile.writable && !logFile.pending) {
+ try {
+ logFile.close(function () {
+ logFile = undefined;
+ logSync = true;
+ process.unsafeExit(code);
+ });
+ if (process.isBun) {
+ setInterval(function () {
+ if (!logFile.writable) {
+ logFile = undefined;
+ logSync = true;
+ process.unsafeExit(code);
+ }
+ }, 50); // Interval
+ }
+ setTimeout(function () {
+ logFile = undefined;
+ logSync = true;
+ process.unsafeExit(code);
+ }, 10000); // timeout
+ } catch (err) {
+ logFile = undefined;
+ logSync = true;
+ process.unsafeExit(code);
+ }
+ } else {
+ logSync = true;
+ process.unsafeExit(code);
+ }
+};
+
+module.exports = (enableLoggingIntoFileParam) => {
+ enableLoggingIntoFile = enableLoggingIntoFileParam;
+ return serverconsole;
+};
diff --git a/src/utils/urlMojibakeFixer.js b/src/utils/urlMojibakeFixer.js
new file mode 100644
index 0000000..92824f5
--- /dev/null
+++ b/src/utils/urlMojibakeFixer.js
@@ -0,0 +1,21 @@
+// Node.JS mojibake URL fixing function
+function fixNodeMojibakeURL(string) {
+ var encoded = "";
+
+ //Encode URLs
+ Buffer.from(string, "latin1").forEach(function (value) {
+ if (value > 127) {
+ encoded +=
+ "%" + (value < 16 ? "0" : "") + value.toString(16).toUpperCase();
+ } else {
+ encoded += String.fromCodePoint(value);
+ }
+ });
+
+ //Upper case the URL encodings
+ return encoded.replace(/%[0-9a-f-A-F]{2}/g, function (match) {
+ return match.toUpperCase();
+ });
+}
+
+module.exports = fixNodeMojibakeURL;
diff --git a/src/utils/urlParser.js b/src/utils/urlParser.js
new file mode 100644
index 0000000..6779707
--- /dev/null
+++ b/src/utils/urlParser.js
@@ -0,0 +1,90 @@
+const url = require("url");
+
+// SVR.JS URL parser function
+function parseURL(uri, prepend) {
+ // Replace newline characters with its respective URL encodings
+ uri = uri.replace(/\r/g, "%0D").replace(/\n/g, "%0A");
+
+ // If URL begins with a slash, prepend a string if available
+ if (prepend && uri[0] == "/") uri = prepend.replace(/\/+$/, "") + uri;
+
+ // Determine if URL has slashes
+ let hasSlashes = uri.indexOf("/") != -1;
+
+ // Parse the URL using regular expression
+ let parsedURI = uri.match(
+ /^(?:([^:]+:)(\/\/)?)?(?:([^@\/?#\*]+)@)?([^:\/?#\*]+|\[[^\*]\/]\])?(?::([0-9]+))?(\*|\/[^?#]*)?(\?[^#]*)?(#[\S\s]*)?/,
+ );
+ // Match 1: protocol
+ // Match 2: slashes after protocol
+ // Match 3: authentication credentials
+ // Match 4: host name
+ // Match 5: port
+ // Match 6: path name
+ // Match 7: query string
+ // Match 8: hash
+
+ // If regular expression didn't match the entire URL, throw an error
+ if (parsedURI[0].length != uri.length) throw new Error("Invalid URL: " + uri);
+
+ // If match 1 is not empty, set the slash variable based on state of match 2
+ if (parsedURI[1]) hasSlashes = parsedURI[2] == "//";
+
+ // If match 6 is empty and URL has slashes, set it to a slash.
+ if (hasSlashes && !parsedURI[6]) parsedURI[6] = "/";
+
+ // If match 4 contains Unicode characters, convert it to Punycode. If the result is an empty string, throw an error
+ if (parsedURI[4] && !parsedURI[4].match(/^[a-zA-Z0-9\.\-]+$/)) {
+ parsedURI[4] = url.domainToASCII(parsedURI[4]);
+ if (!parsedURI[4]) throw new Error("Invalid URL: " + uri);
+ }
+
+ // Create a new URL object
+ let uobject = new url.Url();
+
+ // Populate a URL object
+ if (hasSlashes) uobject.slashes = true;
+ if (parsedURI[1]) uobject.protocol = parsedURI[1];
+ if (parsedURI[3]) uobject.auth = parsedURI[3];
+ if (parsedURI[4]) {
+ uobject.host = parsedURI[4] + (parsedURI[5] ? ":" + parsedURI[5] : "");
+ if (parsedURI[4][0] == "[")
+ uobject.hostname = parsedURI[4].substring(1, parsedURI[4].length - 1);
+ else uobject.hostname = parsedURI[4];
+ }
+ if (parsedURI[5]) uobject.port = parsedURI[5];
+ if (parsedURI[6]) uobject.pathname = parsedURI[6];
+ if (parsedURI[7]) {
+ uobject.search = parsedURI[7];
+ // Parse query strings
+ let qobject = Object.create(null);
+ const parsedQuery = parsedURI[7]
+ .substring(1)
+ .match(/([^&=]*)(?:=([^&]*))?/g);
+ parsedQuery.forEach(function (qp) {
+ if (qp.length > 0) {
+ let parsedQP = qp.match(/([^&=]*)(?:=([^&]*))?/);
+ if (parsedQP) {
+ qobject[parsedQP[1]] = parsedQP[2] ? parsedQP[2] : "";
+ }
+ }
+ });
+ uobject.query = qobject;
+ } else {
+ uobject.query = Object.create(null);
+ }
+ if (parsedURI[8]) uobject.hash = parsedURI[8];
+ if (uobject.pathname)
+ uobject.path = uobject.pathname + (uobject.search ? uobject.search : "");
+ uobject.href =
+ (uobject.protocol ? uobject.protocol + (uobject.slashes ? "//" : "") : "") +
+ (uobject.auth ? uobject.auth + "@" : "") +
+ (uobject.hostname ? uobject.hostname : "") +
+ (uobject.port ? ":" + uobject.port : "") +
+ (uobject.path ? uobject.path : "") +
+ (uobject.hash ? uobject.hash : "");
+
+ return uobject;
+}
+
+module.exports = parseURL;
diff --git a/src/utils/urlSanitizer.js b/src/utils/urlSanitizer.js
index 3ea6979..23a8bde 100644
--- a/src/utils/urlSanitizer.js
+++ b/src/utils/urlSanitizer.js
@@ -1,3 +1,4 @@
+// SVR.JS path sanitizer function
function sanitizeURL(resource, allowDoubleSlashes) {
if (resource == "*" || resource == "") return resource;
// Remove null characters
@@ -7,17 +8,17 @@ function sanitizeURL(resource, allowDoubleSlashes) {
throw new URIError("URI malformed");
// Decode URL-encoded characters while preserving certain characters
resource = resource.replace(/%([0-9a-f]{2})/gi, (match, hex) => {
- var decodedChar = String.fromCharCode(parseInt(hex, 16));
+ const decodedChar = String.fromCharCode(parseInt(hex, 16));
return /(?!["<>^`{|}?#%])[!-~]/.test(decodedChar) ? decodedChar : "%" + hex;
});
// Encode certain characters
resource = resource.replace(/[<>^`{|}]]/g, (character) => {
- var charCode = character.charCodeAt(0);
+ const charCode = character.charCodeAt(0);
return (
"%" + (charCode < 16 ? "0" : "") + charCode.toString(16).toUpperCase()
);
});
- var sanitizedResource = resource;
+ let sanitizedResource = resource;
// Ensure the resource starts with a slash
if (resource[0] != "/") sanitizedResource = "/" + sanitizedResource;
// Convert backslashes to slashes and handle duplicate slashes
diff --git a/tests/utils/generateErrorStack.test.js b/tests/utils/generateErrorStack.test.js
new file mode 100644
index 0000000..25bd0da
--- /dev/null
+++ b/tests/utils/generateErrorStack.test.js
@@ -0,0 +1,44 @@
+const generateErrorStack = require("../../src/utils/generateErrorStack");
+
+describe("generateErrorStack", () => {
+ test("should return the original stack if it is V8-style", () => {
+ const error = new Error("Test error");
+ error.stack = `Error: Test error
+ at Object. (/path/to/file.js:10:15)
+ at Module._compile (internal/modules/cjs/loader.js:1063:30)
+ at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)`;
+
+ const result = generateErrorStack(error);
+ expect(result).toBe(error.stack);
+ });
+
+ test("should generate a new stack if the original stack is SpiderMonkey-style", () => {
+ const error = new Error("Test error");
+ error.stack = `baz@filename.js:10:15
+bar@filename.js:6:3
+foo@filename.js:2:3
+@filename.js:13:1`;
+
+ const result = generateErrorStack(error);
+ expect(result).toContain("Error: Test error");
+ expect(result).toContain(" at baz (filename.js:10:15)");
+ expect(result).toContain(" at bar (filename.js:6:3)");
+ expect(result).toContain(" at foo (filename.js:2:3)");
+ expect(result).toContain(" at filename.js:13:1");
+ });
+
+ test("should generate a new stack if the original stack is JavaScriptCore-style", () => {
+ const error = new Error("Test error");
+ error.stack = `baz@filename.js:10:15
+bar@filename.js:6:3
+foo@filename.js:2:3
+global code@filename.js:13:1`;
+
+ const result = generateErrorStack(error);
+ expect(result).toContain("Error: Test error");
+ expect(result).toContain(" at baz (filename.js:10:15)");
+ expect(result).toContain(" at bar (filename.js:6:3)");
+ expect(result).toContain(" at foo (filename.js:2:3)");
+ expect(result).toContain(" at filename.js:13:1");
+ });
+});
diff --git a/tests/utils/helper.test.js b/tests/utils/helper.test.js
deleted file mode 100644
index b5fe8c7..0000000
--- a/tests/utils/helper.test.js
+++ /dev/null
@@ -1,5 +0,0 @@
-const { add } = require("../../src/utils/helper");
-
-test("adds 1 + 2 to equal 3", () => {
- expect(add(1, 2)).toBe(3);
-});
diff --git a/tests/utils/urlMojibakeFixer.test.js b/tests/utils/urlMojibakeFixer.test.js
new file mode 100644
index 0000000..50373cc
--- /dev/null
+++ b/tests/utils/urlMojibakeFixer.test.js
@@ -0,0 +1,42 @@
+const fixNodeMojibakeURL = require("../../src/utils/urlMojibakeFixer.js");
+
+describe("URL mojibake fixer", () => {
+ test("should return the same string for ASCII characters", () => {
+ expect(fixNodeMojibakeURL("hello world")).toBe("hello world");
+ });
+
+ test("should encode characters with values greater than 127", () => {
+ expect(fixNodeMojibakeURL("é")).toBe("%E9");
+ expect(fixNodeMojibakeURL("ñ")).toBe("%F1");
+ });
+
+ test("should uppercase the URL encodings", () => {
+ expect(fixNodeMojibakeURL("a%e9b")).toBe("a%E9b");
+ });
+
+ test("should handle mixed ASCII and non-ASCII characters", () => {
+ expect(fixNodeMojibakeURL("hello é world ñ")).toBe("hello %E9 world %F1");
+ });
+
+ test("should handle empty string", () => {
+ expect(fixNodeMojibakeURL("")).toBe("");
+ });
+
+ test("should handle strings with special characters", () => {
+ expect(fixNodeMojibakeURL("!@#$%^&*()")).toBe("!@#$%^&*()");
+ });
+
+ test("should handle strings with spaces", () => {
+ expect(fixNodeMojibakeURL("hello world")).toBe("hello world");
+ });
+
+ test("should handle strings with numbers", () => {
+ expect(fixNodeMojibakeURL("12345")).toBe("12345");
+ });
+
+ test("should handle strings with mixed characters", () => {
+ expect(fixNodeMojibakeURL("hello123 é world ñ!@#$%^&*()")).toBe(
+ "hello123 %E9 world %F1!@#$%^&*()",
+ );
+ });
+});
diff --git a/tests/utils/urlParser.test.js b/tests/utils/urlParser.test.js
new file mode 100644
index 0000000..f5953aa
--- /dev/null
+++ b/tests/utils/urlParser.test.js
@@ -0,0 +1,80 @@
+const parseURL = require("../../src/utils/urlParser.js");
+
+describe("URL parser", () => {
+ test("should parse a simple URL", () => {
+ const parsedUrl = parseURL("http://example.com");
+ expect(parsedUrl.protocol).toBe("http:");
+ expect(parsedUrl.hostname).toBe("example.com");
+ expect(parsedUrl.pathname).toBe("/");
+ expect(parsedUrl.path).toBe("/");
+ expect(parsedUrl.href).toBe("http://example.com/");
+ });
+
+ test("should parse a URL with a path", () => {
+ const parsedUrl = parseURL("http://example.com/path/to/resource");
+ expect(parsedUrl.protocol).toBe("http:");
+ expect(parsedUrl.hostname).toBe("example.com");
+ expect(parsedUrl.pathname).toBe("/path/to/resource");
+ expect(parsedUrl.path).toBe("/path/to/resource");
+ expect(parsedUrl.href).toBe("http://example.com/path/to/resource");
+ });
+
+ test("should parse a URL with a query string", () => {
+ const parsedUrl = parseURL("http://example.com/path?query=string");
+ expect(parsedUrl.protocol).toBe("http:");
+ expect(parsedUrl.hostname).toBe("example.com");
+ expect(parsedUrl.pathname).toBe("/path");
+ expect(parsedUrl.search).toBe("?query=string");
+ expect(parsedUrl.query.query).toBe("string");
+ expect(parsedUrl.path).toBe("/path?query=string");
+ expect(parsedUrl.href).toBe("http://example.com/path?query=string");
+ });
+
+ test("should parse a URL with a port", () => {
+ const parsedUrl = parseURL("http://example.com:8080");
+ expect(parsedUrl.protocol).toBe("http:");
+ expect(parsedUrl.hostname).toBe("example.com");
+ expect(parsedUrl.port).toBe("8080");
+ expect(parsedUrl.pathname).toBe("/");
+ expect(parsedUrl.path).toBe("/");
+ expect(parsedUrl.href).toBe("http://example.com:8080/");
+ });
+
+ test("should parse a URL with a username and password", () => {
+ const parsedUrl = parseURL("http://user:pass@example.com");
+ expect(parsedUrl.protocol).toBe("http:");
+ expect(parsedUrl.auth).toBe("user:pass");
+ expect(parsedUrl.hostname).toBe("example.com");
+ expect(parsedUrl.pathname).toBe("/");
+ expect(parsedUrl.path).toBe("/");
+ expect(parsedUrl.href).toBe("http://user:pass@example.com/");
+ });
+
+ test("should parse a URL with a fragment", () => {
+ const parsedUrl = parseURL("http://example.com/path#fragment");
+ expect(parsedUrl.protocol).toBe("http:");
+ expect(parsedUrl.hostname).toBe("example.com");
+ expect(parsedUrl.pathname).toBe("/path");
+ expect(parsedUrl.hash).toBe("#fragment");
+ expect(parsedUrl.path).toBe("/path");
+ expect(parsedUrl.href).toBe("http://example.com/path#fragment");
+ });
+
+ test("should parse a URL with all components", () => {
+ const parsedUrl = parseURL(
+ "http://user:pass@example.com:8080/path/to/resource?query=string#fragment",
+ );
+ expect(parsedUrl.protocol).toBe("http:");
+ expect(parsedUrl.auth).toBe("user:pass");
+ expect(parsedUrl.hostname).toBe("example.com");
+ expect(parsedUrl.port).toBe("8080");
+ expect(parsedUrl.pathname).toBe("/path/to/resource");
+ expect(parsedUrl.search).toBe("?query=string");
+ expect(parsedUrl.query.query).toBe("string");
+ expect(parsedUrl.hash).toBe("#fragment");
+ expect(parsedUrl.path).toBe("/path/to/resource?query=string");
+ expect(parsedUrl.href).toBe(
+ "http://user:pass@example.com:8080/path/to/resource?query=string#fragment",
+ );
+ });
+});