From dd344ecca39010ed0e7f191f725fa40ccaaec698 Mon Sep 17 00:00:00 2001 From: Dorian Niemiec Date: Fri, 23 Aug 2024 21:25:23 +0200 Subject: [PATCH] Expand core middleware, add URL sanitizer middleware, and add logging to the middleware error handling. --- src/index.js | 11 +- src/middleware/core.js | 594 ++++++++++++++++++++++++++++++--- src/middleware/urlSanitizer.js | 63 ++++ 3 files changed, 625 insertions(+), 43 deletions(-) create mode 100644 src/middleware/urlSanitizer.js diff --git a/src/index.js b/src/index.js index 62c9cb4..38d90ff 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,7 @@ 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; @@ -27,11 +25,13 @@ 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"; +if (!configJSON.customHeaders) configJSON.customHeaders = {}; const serverconsole = serverconsoleConstructor(configJSON.enableLogging); let middleware = [ - require("./middleware/core.js") + require("./middleware/core.js"), + require("./middleware/urlSanitizer.js") ]; function addMiddleware(mw) { @@ -66,8 +66,11 @@ function requestHandler(req, res) { try { currentMiddleware(req, res, logFacilities, config, next); } catch (err) { - if (res.error) res.error(500); + if (res.error) res.error(500, err); else { + logFacilities.errmessage("There was an error while processing the request!"); + logFacilities.errmessage("Stack:"); + logFacilities.errmessage(err.stack); 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") }); diff --git a/src/middleware/core.js b/src/middleware/core.js index c2ad0f0..0b1cf39 100644 --- a/src/middleware/core.js +++ b/src/middleware/core.js @@ -2,10 +2,295 @@ const http = require("http"); const fs = require("fs"); const generateErrorStack = require("../utils/generateErrorStack.js"); const serverHTTPErrorDescs = require("../res/httpErrorDescriptions.js"); +const fixNodeMojibakeURL = require("../utils/urlMojibakeFixer.js"); + +if (!process.err4xxcounter) process.err4xxcounter = 0; +if (!process.err5xxcounter) process.err5xxcounter = 0; +if (!process.reqcounter) process.reqcounter = 0; module.exports = (req, res, logFacilities, config, next) => { - // TODO: proxy + const matchHostname = (hostname) => { + if (typeof hostname == "undefined" || hostname == "*") { + return true; + } else if ( + req.headers.host && + hostname.indexOf("*.") == 0 && + hostname != "*." + ) { + const hostnamesRoot = hostname.substring(2); + if ( + req.headers.host == hostnamesRoot || + (req.headers.host.length > hostnamesRoot.length && + req.headers.host.indexOf("." + hostnamesRoot) == + req.headers.host.length - hostnamesRoot.length - 1) + ) { + return true; + } + } else if (req.headers.host && req.headers.host == hostname) { + return true; + } + return false; + }; + + config.getCustomHeaders = () => { + let ph = Object.assign(config.customHeaders); + if (config.customHeadersVHost) { + let vhostP = null; + config.customHeadersVHost.every(function (vhost) { + if ( + matchHostname(vhost.host) && + ipMatch(vhost.ip, req.socket ? req.socket.localAddress : undefined) + ) { + vhostP = vhost; + return false; + } else { + return true; + } + }); + if (vhostP && vhostP.headers) { + const phNu = Object.assign(vhostP.headers); + Object.keys(phNu).forEach(function (phNuK) { + ph[phNuK] = phNu[phNuK]; + }); + } + } + Object.keys(ph).forEach(function (phk) { + if (typeof ph[phk] == "string") + ph[phk] = ph[phk].replace(/\{path\}/g, req.url); + }); + ph["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"; + return ph; + }; + + // Make HTTP/1.x API-based scripts compatible with HTTP/2.0 API + if (config.enableHTTP2 == true && req.httpVersion == "2.0") { + // Set HTTP/1.x methods (to prevent process warnings) + res.writeHeadNodeApi = res.writeHead; + res.setHeaderNodeApi = res.setHeader; + + res.writeHead = function (a, b, c) { + let table = c; + if (typeof b == "object") table = b; + if (table == undefined) table = this.tHeaders; + if (table == undefined) table = {}; + table = Object.assign(table); + Object.keys(table).forEach(function (key) { + const al = key.toLowerCase(); + if ( + al == "transfer-encoding" || + al == "connection" || + al == "keep-alive" || + al == "upgrade" + ) + delete table[key]; + }); + if (res.stream && res.stream.destroyed) { + return false; + } else { + return res.writeHeadNodeApi(a, table); + } + }; + res.setHeader = function (headerName, headerValue) { + const al = headerName.toLowerCase(); + if ( + al != "transfer-encoding" && + al != "connection" && + al != "keep-alive" && + al != "upgrade" + ) + return res.setHeaderNodeApi(headerName, headerValue); + return false; + }; + + // Set HTTP/1.x headers + if (!req.headers.host) req.headers.host = req.headers[":authority"]; + if (!req.url) req.url = req.headers[":path"]; + if (!req.protocol) req.protocol = req.headers[":scheme"]; + if (!req.method) req.method = req.headers[":method"]; + if ( + req.headers[":path"] == undefined || + req.headers[":method"] == undefined + ) { + let err = new Error( + 'Either ":path" or ":method" pseudoheader is missing.', + ); + if (Buffer.alloc) err.rawPacket = Buffer.alloc(0); + return; + // TODO: reqerrhandler(err, req.socket, fromMain); + } + } + + /*if (req.headers["x-svr-js-from-main-thread"] == "true" && req.socket && (!req.socket.remoteAddress || req.socket.remoteAddress == "::1" || req.socket.remoteAddress == "::ffff:127.0.0.1" || req.socket.remoteAddress == "127.0.0.1" || req.socket.remoteAddress == "localhost" || req.socket.remoteAddress == host || req.socket.remoteAddress == "::ffff:" + host)) { + var headers = config.getCustomHeaders(); + res.writeHead(204, http.STATUS_CODES[204], headers); + res.end(); + return; + }*/ + + req.url = fixNodeMojibakeURL(req.url); + + var headWritten = false; + var lastStatusCode = null; + res.writeHeadNative = res.writeHead; + res.writeHead = function (code, codeDescription, headers) { + if ( + !( + headWritten && + process.isBun && + code === lastStatusCode && + codeDescription === undefined && + codeDescription === undefined + ) + ) { + if (headWritten) { + process.emitWarning("res.writeHead called multiple times.", { + code: "WARN_SVRJS_MULTIPLE_WRITEHEAD", + }); + return res; + } else { + headWritten = true; + } + if (code >= 400 && code <= 599) { + if (code >= 400 && code <= 499) process.err4xxcounter++; + else if (code >= 500 && code <= 599) process.err5xxcounter++; + logFacilities.errmessage( + "Server responded with " + code.toString() + " code.", + ); + } else { + logFacilities.resmessage( + "Server responded with " + code.toString() + " code.", + ); + } + if (typeof codeDescription != "string" && http.STATUS_CODES[code]) { + if (!headers) headers = codeDescription; + codeDescription = http.STATUS_CODES[code]; + } + lastStatusCode = code; + } + res.writeHeadNative(code, codeDescription, headers); + }; + + var finished = false; + res.on("finish", function () { + if (!finished) { + finished = true; + logFacilities.locmessage("Client disconnected."); + } + }); + res.on("close", function () { + if (!finished) { + finished = true; + logFacilities.locmessage("Client disconnected."); + } + }); + + // TODO: fromMain + let fromMain = true; + req.isProxy = false; + if (req.url[0] != "/" && req.url != "*") req.isProxy = true; + logFacilities.locmessage( + "Somebody connected to " + + (config.secure && fromMain + ? (typeof config.sport == "number" ? "port " : "socket ") + config.sport + : (typeof config.port == "number" ? "port " : "socket ") + + config.port) + + "...", + ); + + if (req.socket == null) { + logFacilities.errmessage("Client socket is null!!!"); + return; + } + + // Set up X-Forwarded-For + let reqip = req.socket.remoteAddress; + let reqport = req.socket.remotePort; + let oldip = ""; + let oldport = ""; + let isForwardedValid = true; + if (config.enableIPSpoofing) { + if (req.headers["x-forwarded-for"] != undefined) { + let preparedReqIP = req.headers["x-forwarded-for"] + .split(",")[0] + .replace(/ /g, ""); + let preparedReqIPvalid = net.isIP(preparedReqIP); + if (preparedReqIPvalid) { + if ( + preparedReqIPvalid == 4 && + req.socket.remoteAddress && + req.socket.remoteAddress.indexOf(":") > -1 + ) + preparedReqIP = "::ffff:" + preparedReqIP; + reqip = preparedReqIP; + reqport = null; + try { + oldport = req.socket.remotePort; + oldip = req.socket.remoteAddress; + req.socket.realRemotePort = reqport; + req.socket.realRemoteAddress = reqip; + req.socket.originalRemotePort = oldport; + req.socket.originalRemoteAddress = oldip; + res.socket.realRemotePort = reqport; + res.socket.realRemoteAddress = reqip; + res.socket.originalRemotePort = oldport; + res.socket.originalRemoteAddress = oldip; + } catch (err) { + // Address setting failed + } + } else { + isForwardedValid = false; + } + } + } + + process.reqcounter++; + + // Process the Host header + var oldHostHeader = req.headers.host; + if (typeof req.headers.host == "string") { + req.headers.host = req.headers.host.toLowerCase(); + if (!req.headers.host.match(/^\.+$/)) + req.headers.host = req.headers.host.replace(/\.$/g, ""); + } + + logFacilities.reqmessage( + "Client " + + (!reqip || reqip == "" + ? "[unknown client]" + : reqip + + (reqport && reqport !== 0 && reqport != "" ? ":" + reqport : "")) + + " wants " + + (req.method == "GET" + ? "content in " + : req.method == "POST" + ? "to post content in " + : req.method == "PUT" + ? "to add content in " + : req.method == "DELETE" + ? "to delete content in " + : req.method == "PATCH" + ? "to patch content in " + : "to access content using " + req.method + " method in ") + + (req.headers.host == undefined || req.isProxy ? "" : req.headers.host) + + req.url, + ); + if (req.headers["user-agent"] != undefined) + logFacilities.reqmessage("Client uses " + req.headers["user-agent"]); + if (oldHostHeader && oldHostHeader != req.headers.host) + logFacilities.resmessage( + "Host name rewritten: " + oldHostHeader + " => " + req.headers.host, + ); // Server error calling method res.error = (errorCode, extName, stack, ch) => { @@ -18,37 +303,54 @@ module.exports = (req, res, logFacilities, config, next) => { ch = stack; stack = extName; extName = undefined; - } else if (typeof extName !== "string" && extName !== null && 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]") { + 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."); + } 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()); + 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); } - } catch (err2) { - res.error(500, err2); - } - }); + }, + ); } else { try { callback(config.page404); @@ -58,17 +360,21 @@ module.exports = (req, res, logFacilities, config, next) => { } }); } else { - fs.access("." + errorCode.toString(), fs.constants.F_OK, function (err) { - try { - if (err) { - callback(errorCode.toString() + ".html"); - } else { - callback("." + errorCode.toString()); + 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); } - } catch (err2) { - res.error(500, err2); - } - }); + }, + ); } } } @@ -79,7 +385,13 @@ module.exports = (req, res, logFacilities, config, next) => { return; } - if (list[_i].scode != errorCode || !(matchHostname(list[_i].host) && ipMatch(list[_i].ip, req.socket ? req.socket.localAddress : undefined))) { + 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 { @@ -95,11 +407,15 @@ module.exports = (req, res, logFacilities, config, next) => { 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 (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( + "There was an error while processing the request!", + ); logFacilities.errmessage("Stack:"); logFacilities.errmessage(stack); } @@ -112,19 +428,93 @@ module.exports = (req, res, logFacilities, config, next) => { res.error(501, extName, stack); } else { // Process custom headers if provided - let cheaders = { ...config.customHeaders, ...ch }; + let cheaders = { ...config.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"; + 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 + 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 @@ -143,13 +533,92 @@ module.exports = (req, res, logFacilities, config, next) => { } 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.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) => { @@ -160,13 +629,19 @@ module.exports = (req, res, logFacilities, config, next) => { if (typeof isTemporary == "object") customHeaders = isTemporary; // If customHeaders are not provided, get the default custom headers - if (customHeaders === undefined) customHeaders = Object.assign(config.customHeaders); + if (customHeaders === undefined) customHeaders = config.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); + 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); @@ -179,14 +654,55 @@ module.exports = (req, res, logFacilities, config, next) => { // Return from the function return; + }; + + if (req.url == "*") { + // Handle "*" URL + if (req.method == "OPTIONS") { + // Respond with list of methods + let hdss = config.getCustomHeaders(); + hdss["Allow"] = "GET, POST, HEAD, OPTIONS"; + res.writeHead(204, http.STATUS_CODES[204], hdss); + res.end(); + return; + } else { + // SVR.JS doesn't understand that request, so throw an 400 error + callServerError(400); + return; + } + } + + if (req.method == "CONNECT") { + // CONNECT requests should be handled in "connect" event. + callServerError(501); + logFacilities.errmessage( + "CONNECT requests aren't supported. Your JS runtime probably doesn't support 'connect' handler for HTTP library.", + ); + return; + } + + if (!isForwardedValid) { + logFacilities.errmessage("X-Forwarded-For header is invalid."); + res.error(400); + return; } try { - req.parsedURL = new URL(req.url, "http" + (req.socket.encrypted ? "s" : "") + "://" + (req.headers.host ? req.headers.host : (domain ? domain : "unknown.invalid"))); + req.parsedURL = new URL( + req.url, + "http" + + (req.socket.encrypted ? "s" : "") + + "://" + + (req.headers.host + ? req.headers.host + : config.domain + ? config.domain + : "unknown.invalid"), + ); } catch (err) { res.error(400, err); return; } next(); -} \ No newline at end of file +}; diff --git a/src/middleware/urlSanitizer.js b/src/middleware/urlSanitizer.js new file mode 100644 index 0000000..eddd133 --- /dev/null +++ b/src/middleware/urlSanitizer.js @@ -0,0 +1,63 @@ +const { emitWarning } = require("process"); +const sanitizeURL = require("../utils/urlSanitizer.js"); +const url = require("url"); + +module.exports = (req, res, logFacilities, config, next) => { + let href = req.parsedURL.pathname + req.parsedURL.search; + + // Sanitize URL + let sanitizedHref = sanitizeURL(href, config.allowDoubleSlashes); + let preparedReqUrl = + req.parsedURL.pathname + req.parsedURL.search + req.parsedURL.hash; + + // Check if URL is "dirty" + if (href != sanitizedHref && !req.isProxy) { + let sanitizedURL = new url.Url(); + sanitizedURL.path = null; + sanitizedURL.href = null; + sanitizedURL.pathname = sanitizedHref; + sanitizedURL.hostname = null; + sanitizedURL.host = null; + sanitizedURL.port = null; + sanitizedURL.protocol = null; + sanitizedURL.slashes = null; + sanitizedURL = url.format(sanitizedURL); + logFacilities.resmessage( + "URL sanitized: " + req.url + " => " + sanitizedURL, + ); + if (config.rewriteDirtyURLs) { + req.url = sanitizedURL; + try { + req.parsedURL = new URL( + req.url, + "http" + + (req.socket.encrypted ? "s" : "") + + "://" + + (req.headers.host + ? req.headers.host + : config.domain + ? config.domain + : "unknown.invalid"), + ); + } catch (err) { + res.error(400, err); + return; + } + } else { + res.redirect(sanitizedURL, false); + return; + } + } else if (req.url != preparedReqUrl && !req.isProxy) { + logFacilities.resmessage( + "URL sanitized: " + req.url + " => " + preparedReqUrl, + ); + if (config.rewriteDirtyURLs) { + req.url = preparedReqUrl; + } else { + res.redirect(preparedReqUrl, false); + return; + } + } + + next(); +};