1
0
Fork 0
forked from svrjs/svrjs

Add some core functionality to SVR.JS

This commit is contained in:
Dorian Niemiec 2024-08-23 19:58:15 +02:00
parent fbdb3f93d4
commit ee4c3dcda6
16 changed files with 1167 additions and 22 deletions

View file

@ -119,7 +119,7 @@ esbuild.build({
bundle: true, bundle: true,
outfile: "dist/svr.js", // Output file outfile: "dist/svr.js", // Output file
platform: "node", platform: "node",
target: "es2020", target: "es2017",
plugins: [ plugins: [
esbuildCopyPlugin.copy({ esbuildCopyPlugin.copy({
resolveFrom: __dirname, resolveFrom: __dirname,

View file

@ -26,5 +26,5 @@ export default [
} }
}, },
pluginJs.configs.recommended, pluginJs.configs.recommended,
eslintPluginPrettierRecommended, eslintPluginPrettierRecommended
]; ];

View file

@ -1,4 +1,244 @@
var http = require("http"); const http = require("http");
http.createServer((req, res) => { const fs = require("fs");
res.end("Hello World!"); const cluster = require("./utils/clusterBunShim.js"); // Cluster module with shim for Bun
}).listen(3000); 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")).replace(/{errorDesc}/g, serverHTTPErrorDescs[errorCode]).replace(/{stack}/g, stack.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\r\n/g, "<br/>").replace(/\n/g, "<br/>").replace(/\r/g, "<br/>").replace(/ {2}/g, "&nbsp;&nbsp;")).replace(/{path}/g, req.url.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")).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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") + ((req.headers.host == undefined || isProxy) ? "" : " on " + String(req.headers.host).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"))).replace(/{contact}/g, serverAdmin.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").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(("<!DOCTYPE html><html><head><title>{errorMessage}</title><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" /><style>html{background-color:#dfffdf;color:#000000;font-family:FreeSans, Helvetica, Tahoma, Verdana, Arial, sans-serif;margin:0.75em}body{background-color:#ffffff;padding:0.5em 0.5em 0.1em;margin:0.5em auto;width:90%;max-width:800px;-webkit-box-shadow:0 5px 10px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:0 5px 10px 0 rgba(0, 0, 0, 0.15);box-shadow:0 5px 10px 0 rgba(0, 0, 0, 0.15)}h1{text-align:center;font-size:2.25em;margin:0.3em 0 0.5em}code{background-color:#dfffdf;-webkit-box-shadow:0 2px 4px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 2px 4px 0 rgba(0, 0, 0, 0.1);box-shadow:0 2px 4px 0 rgba(0, 0, 0, 0.1);display:block;padding:0.2em;font-family:\"DejaVu Sans Mono\", \"Bitstream Vera Sans Mono\", Hack, Menlo, Consolas, Monaco, monospace;font-size:0.85em;margin:auto;width:95%;max-width:600px}table{width:95%;border-collapse:collapse;margin:auto;overflow-wrap:break-word;word-wrap:break-word;word-break:break-all;word-break:break-word;position:relative;z-index:0}table tbody{background-color:#ffffff;color:#000000}table tbody:after{-webkit-box-shadow:0 4px 8px 0 rgba(0, 0, 0, 0.175);-moz-box-shadow:0 4px 8px 0 rgba(0, 0, 0, 0.175);box-shadow:0 4px 8px 0 rgba(0, 0, 0, 0.175);content:' ';position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}table img{margin:0;display:inline}th,tr{padding:0.15em;text-align:center}th{background-color:#007000;color:#ffffff}th a{color:#ffffff}td,th{padding:0.225em}td{text-align:left}tr:nth-child(odd){background-color:#dfffdf}hr{color:#ffffff}@media screen and (prefers-color-scheme: dark){html{background-color:#002000;color:#ffffff}body{background-color:#000f00;-webkit-box-shadow:0 5px 10px 0 rgba(127, 127, 127, 0.15);-moz-box-shadow:0 5px 10px 0 rgba(127, 127, 127, 0.15);box-shadow:0 5px 10px 0 rgba(127, 127, 127, 0.15)}code{background-color:#002000;-webkit-box-shadow:0 2px 4px 0 rgba(127, 127, 127, 0.1);-moz-box-shadow:0 2px 4px 0 rgba(127, 127, 127, 0.1);box-shadow:0 2px 4px 0 rgba(127, 127, 127, 0.1)}a{color:#ffffff}a:hover{color:#00ff00}table tbody{background-color:#000f00;color:#ffffff}table tbody:after{-webkit-box-shadow:0 4px 8px 0 rgba(127, 127, 127, 0.175);-moz-box-shadow:0 4px 8px 0 rgba(127, 127, 127, 0.175);box-shadow:0 4px 8px 0 rgba(127, 127, 127, 0.175)}tr:nth-child(odd){background-color:#002000}}</style></head><body><h1>{errorMessage}</h1><p>{errorDesc}</p>" + ((additionalError == 404) ? "" : "<p>Additionally, a {additionalError} error occurred while loading an error page.</p>") + "<p><i>{server}</i></p></body></html>").replace(/{errorMessage}/g, errorCode.toString() + " " + http.STATUS_CODES[errorCode].replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")).replace(/{errorDesc}/g, serverHTTPErrorDescs[errorCode]).replace(/{stack}/g, stack.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\r\n/g, "<br/>").replace(/\n/g, "<br/>").replace(/\r/g, "<br/>").replace(/ {2}/g, "&nbsp;&nbsp;")).replace(/{path}/g, req.url.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")).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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") + ((req.headers.host == undefined || isProxy) ? "" : " on " + String(req.headers.host).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"))).replace(/{contact}/g, serverAdmin.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").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);

View file

@ -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: </p><code>{stack}</code><p>You may need to contact the server administrator at <i>{contact}</i>.",
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.</p><p>You may need to contact the server administrator at <i>{contact}</i>.",
503: "The service provided by the server is currently unavailable, possibly due to maintenance downtime or capacity problems. Please try again later.</p><p>You may need to contact the server administrator at <i>{contact}</i>.",
504: "The server couldn't get a response in time while it was acting as a gateway.</p><p>You may need to contact the server administrator at <i>{contact}</i>.",
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.</p><p>You may need to contact the server administrator at <i>{contact}</i>.",
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.</p><p>You may need to contact the server administrator at <i>{contact}</i>.",
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;

230
src/utils/clusterBunShim.js Normal file
View file

@ -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;

View file

@ -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 == ""
? "<anonymous>"
: 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;

31
src/utils/getOS.js Normal file
View file

@ -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;

View file

@ -1,8 +0,0 @@
// src/utils/helper.js
function add(a, b) {
return a + b;
}
module.exports = {
add,
};

277
src/utils/serverconsole.js Normal file
View file

@ -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;
};

View file

@ -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;

90
src/utils/urlParser.js Normal file
View file

@ -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;

View file

@ -1,3 +1,4 @@
// SVR.JS path sanitizer function
function sanitizeURL(resource, allowDoubleSlashes) { function sanitizeURL(resource, allowDoubleSlashes) {
if (resource == "*" || resource == "") return resource; if (resource == "*" || resource == "") return resource;
// Remove null characters // Remove null characters
@ -7,17 +8,17 @@ function sanitizeURL(resource, allowDoubleSlashes) {
throw new URIError("URI malformed"); throw new URIError("URI malformed");
// Decode URL-encoded characters while preserving certain characters // Decode URL-encoded characters while preserving certain characters
resource = resource.replace(/%([0-9a-f]{2})/gi, (match, hex) => { 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; return /(?!["<>^`{|}?#%])[!-~]/.test(decodedChar) ? decodedChar : "%" + hex;
}); });
// Encode certain characters // Encode certain characters
resource = resource.replace(/[<>^`{|}]]/g, (character) => { resource = resource.replace(/[<>^`{|}]]/g, (character) => {
var charCode = character.charCodeAt(0); const charCode = character.charCodeAt(0);
return ( return (
"%" + (charCode < 16 ? "0" : "") + charCode.toString(16).toUpperCase() "%" + (charCode < 16 ? "0" : "") + charCode.toString(16).toUpperCase()
); );
}); });
var sanitizedResource = resource; let sanitizedResource = resource;
// Ensure the resource starts with a slash // Ensure the resource starts with a slash
if (resource[0] != "/") sanitizedResource = "/" + sanitizedResource; if (resource[0] != "/") sanitizedResource = "/" + sanitizedResource;
// Convert backslashes to slashes and handle duplicate slashes // Convert backslashes to slashes and handle duplicate slashes

View file

@ -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.<anonymous> (/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");
});
});

View file

@ -1,5 +0,0 @@
const { add } = require("../../src/utils/helper");
test("adds 1 + 2 to equal 3", () => {
expect(add(1, 2)).toBe(3);
});

View file

@ -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!@#$%^&*()",
);
});
});

View file

@ -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",
);
});
});