From 000ae75904ca74ada1f83001bf951aacbfb8b91d Mon Sep 17 00:00:00 2001 From: Dorian Niemiec Date: Fri, 23 Aug 2024 20:43:09 +0200 Subject: [PATCH] Add middleware functionality. Also replace JSON.parse with JSON.stringify with Object.assign. --- src/index.js | 235 +++++++----------------------------- src/middleware/core.js | 192 +++++++++++++++++++++++++++++ src/utils/clusterBunShim.js | 4 +- 3 files changed, 236 insertions(+), 195 deletions(-) create mode 100644 src/middleware/core.js diff --git a/src/index.js b/src/index.js index 20f7d69..62c9cb4 100644 --- a/src/index.js +++ b/src/index.js @@ -2,8 +2,8 @@ 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 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; @@ -17,26 +17,28 @@ if (!fs.existsSync(__dirname + "/temp")) fs.mkdirSync(__dirname + "/temp"); const serverconsoleConstructor = require("./utils/serverconsole.js"); -let config = {}; +let configJSON = {}; // 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 {}; +if (!configJSON.page404) configJSON.page404 = "404.html" +if (!configJSON.errorPages) configJSON.errorPages = []; +if (!configJSON.stackHidden) configJSON.stackHidden = true; +if (!configJSON.exposeServerVersion) configJSON.exposeServerVersion = false; +if (!configJSON.exposeModsInErrorPages) configJSON.exposeModsInErrorPages = false; +if (!configJSON.enableLogging) configJSON.enableLogging = true; +if (!configJSON.serverAdministratorEmail) configJSON.serverAdministratorEmail = "webmaster@svrjs.org"; + +const serverconsole = serverconsoleConstructor(configJSON.enableLogging); + +let middleware = [ + require("./middleware/core.js") +]; + +function addMiddleware(mw) { + middleware.push(mw); } -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); @@ -52,192 +54,39 @@ function requestHandler(req, res) { 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."); - } + // SVR.JS configuration object (modified) + const config = Object.assign(configJSON); - // 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."); - } + let index = 0; - 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); + // Call the next middleware function + const next = () => { + const currentMiddleware = middleware[index++]; + if (currentMiddleware) { + try { + currentMiddleware(req, res, logFacilities, config, next); + } catch (err) { + if (res.error) res.error(500); 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); - } - }); - } + res.writeHead(500, "Internal Server Error", { + Server: (config.exposeServerVersion ? "SVR.JS/" + version + " (" + getOS() + "; " + (process.isBun ? ("Bun/v" + process.versions.bun + "; like Node.JS/" + process.version) : ("Node.JS/" + process.version)) + ")" : "SVR.JS") + }); + res.end("Error while executing the request handler"); } } - - 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); - } + } else { + if (res.error) res.error(404); + else { + res.writeHead(404, "Not Found", { + Server: (config.exposeServerVersion ? "SVR.JS/" + version + " (" + getOS() + "; " + (process.isBun ? ("Bun/v" + process.versions.bun + "; like Node.JS/" + process.version) : ("Node.JS/" + process.version)) + ")" : "SVR.JS") }); + res.end("Request handler missing"); } } - - 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); + // Handle middleware + next(); } // Create HTTP server diff --git a/src/middleware/core.js b/src/middleware/core.js new file mode 100644 index 0000000..c2ad0f0 --- /dev/null +++ b/src/middleware/core.js @@ -0,0 +1,192 @@ +const http = require("http"); +const fs = require("fs"); +const generateErrorStack = require("../utils/generateErrorStack.js"); +const serverHTTPErrorDescs = require("../res/httpErrorDescriptions.js"); + +module.exports = (req, res, logFacilities, config, next) => { + // TODO: proxy + req.isProxy = false; + + // 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(config.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(config.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(config.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) { + logFacilities.errmessage("There was an error while processing the request!"); + logFacilities.errmessage("Stack:"); + logFacilities.errmessage(stack); + } + + // Hide the error stack if specified + if (config.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 = { ...config.customHeaders, ...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, "" + ((config.exposeServerVersion ? "SVR.JS/" + version + " (" + getOS() + "; " + (process.isBun ? ("Bun/v" + process.versions.bun + "; like Node.JS/" + process.version) : ("Node.JS/" + process.version)) + ")" : "SVR.JS") + ((!config.exposeModsInErrorPages || extName == undefined) ? "" : " " + extName)).replace(/&/g, "&").replace(//g, ">") + ((req.headers.host == undefined || req.isProxy) ? "" : " on " + String(req.headers.host).replace(/&/g, "&").replace(//g, ">"))).replace(/{contact}/g, config.serverAdministratorEmail.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, "" + ((config.exposeServerVersion ? "SVR.JS/" + version + " (" + getOS() + "; " + (process.isBun ? ("Bun/v" + process.versions.bun + "; like Node.JS/" + process.version) : ("Node.JS/" + process.version)) + ")" : "SVR.JS") + ((!config.exposeModsInErrorPages || extName == undefined) ? "" : " " + extName)).replace(/&/g, "&").replace(//g, ">") + ((req.headers.host == undefined || req.isProxy) ? "" : " on " + String(req.headers.host).replace(/&/g, "&").replace(//g, ">"))).replace(/{contact}/g, config.serverAdministratorEmail.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 = Object.assign(config.customHeaders); + + // 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.resmessage("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; + } + + next(); +} \ No newline at end of file diff --git a/src/utils/clusterBunShim.js b/src/utils/clusterBunShim.js index 1c6e34e..a5162dc 100644 --- a/src/utils/clusterBunShim.js +++ b/src/utils/clusterBunShim.js @@ -85,9 +85,9 @@ cluster.bunShim = function () { cluster.workers = {}; cluster.fork = function (env) { const child_process = require("child_process"); - let newEnvironment = JSON.parse(JSON.stringify(env ? env : process.env)); + let newEnvironment = Object.assign(env ? env : process.env); newEnvironment.NODE_UNIQUE_ID = cluster._workersCounter; - let newArguments = JSON.parse(JSON.stringify(process.argv)); + let newArguments = Object.assign(process.argv); let command = newArguments.shift(); let newWorker = child_process.spawn(command, newArguments, { env: newEnvironment,