diff --git a/package.json b/package.json index 2351ed2..d6289c4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "node esbuild.config.js", "dev": "npm run build && npm run start", - "lint": "eslint --no-error-on-unmatched-pattern src/**/*.js src/*.js tests/**/*.test.js tests/**/*.js utils/**/*.js", + "lint": "eslint --no-error-on-unmatched-pattern src/**/*.js src/*.js tests/**/*.test.js tests/**/*.js tests/*.test.js tests/*.js utils/**/*.js utils/*.js", "lint:fix": "npm run lint -- --fix", "start": "node dist/svr.js", "test": "jest" diff --git a/src/utils/forbiddenPaths.js b/src/utils/forbiddenPaths.js index b91a4f7..d2d4007 100644 --- a/src/utils/forbiddenPaths.js +++ b/src/utils/forbiddenPaths.js @@ -22,57 +22,50 @@ function getInitializePath(to) { } } -// Function to check if URL path name is a forbidden path. function isForbiddenPath(decodedHref, match) { const forbiddenPath = forbiddenPaths[match]; if (!forbiddenPath) return false; + + const isWin32 = os.platform() === "win32"; + const decodedHrefLower = isWin32 ? decodedHref.toLowerCase() : null; + if (typeof forbiddenPath === "string") { - return ( - decodedHref === forbiddenPath || - (os.platform() === "win32" && - decodedHref.toLowerCase() === forbiddenPath.toLowerCase()) - ); + return isWin32 + ? decodedHrefLower === forbiddenPath.toLowerCase() + : decodedHref === forbiddenPath; } + if (typeof forbiddenPath === "object") { - return forbiddenPath.some((forbiddenPathSingle) => { - return ( - decodedHref === forbiddenPathSingle || - (os.platform() === "win32" && - decodedHref.toLowerCase() === forbiddenPathSingle.toLowerCase()) - ); - }); + return isWin32 + ? forbiddenPath.some((path) => decodedHrefLower === path.toLowerCase()) + : forbiddenPath.includes(decodedHref); } + return false; } -// Function to check if URL path name is index of one of defined forbidden paths. function isIndexOfForbiddenPath(decodedHref, match) { const forbiddenPath = forbiddenPaths[match]; if (!forbiddenPath) return false; + + const isWin32 = os.platform() === "win32"; + const decodedHrefLower = isWin32 ? decodedHref.toLowerCase() : null; + if (typeof forbiddenPath === "string") { - return ( - decodedHref === forbiddenPath || - decodedHref.indexOf(forbiddenPath + "/") === 0 || - (os.platform() === "win32" && - (decodedHref.toLowerCase() === forbiddenPath.toLowerCase() || - decodedHref - .toLowerCase() - .indexOf(forbiddenPath.toLowerCase() + "/") === 0)) - ); + const forbiddenPathLower = isWin32 ? forbiddenPath.toLowerCase() : null; + return isWin32 + ? decodedHrefLower.indexOf(forbiddenPathLower) == 0 + : decodedHref.indexOf(forbiddenPath) == 0; } + if (typeof forbiddenPath === "object") { - return forbiddenPath.some((forbiddenPathSingle) => { - return ( - decodedHref === forbiddenPathSingle || - decodedHref.indexOf(forbiddenPathSingle + "/") === 0 || - (os.platform() === "win32" && - (decodedHref.toLowerCase() === forbiddenPathSingle.toLowerCase() || - decodedHref - .toLowerCase() - .indexOf(forbiddenPathSingle.toLowerCase() + "/") === 0)) - ); - }); + return isWin32 + ? forbiddenPath.some( + (path) => decodedHrefLower.indexOf(path.toLowerCase()) == 0, + ) + : forbiddenPath.some((path) => decodedHref.indexOf(path) == 0); } + return false; } diff --git a/src/utils/legacyModWrapper.js b/src/utils/legacyModWrapper.js index 12994e0..2ce6504 100644 --- a/src/utils/legacyModWrapper.js +++ b/src/utils/legacyModWrapper.js @@ -13,6 +13,7 @@ module.exports = (legacyMod) => { let middleware = (req, res, logFacilities, config, next) => { let ext = req.parsedURL.pathname.match(/[^/]\.([^.]+)$/); if (!ext) ext = ""; + else ext = ext[1].toLowerCase(); // Function to parse incoming POST data from the request const parsePostData = (options, callback) => { diff --git a/src/utils/matchHostname.js b/src/utils/matchHostname.js index b2c8c46..36b3ce4 100644 --- a/src/utils/matchHostname.js +++ b/src/utils/matchHostname.js @@ -1,17 +1,17 @@ function matchHostname(hostname, reqHostname) { - if (typeof hostname == "undefined" || hostname == "*") { + if (typeof hostname === "undefined" || hostname === "*") { return true; - } else if (reqHostname && hostname.indexOf("*.") == 0 && hostname != "*.") { + } else if (reqHostname && hostname.indexOf("*.") == 0 && hostname !== "*.") { const hostnamesRoot = hostname.substring(2); if ( - reqHostname == hostnamesRoot || + reqHostname === hostnamesRoot || (reqHostname.length > hostnamesRoot.length && - reqHostname.indexOf("." + hostnamesRoot) == + reqHostname.indexOf("." + hostnamesRoot) === reqHostname.length - hostnamesRoot.length - 1) ) { return true; } - } else if (reqHostname && reqHostname == hostname) { + } else if (reqHostname && reqHostname === hostname) { return true; } return false; diff --git a/src/utils/sizify.js b/src/utils/sizify.js index 203746c..688ed54 100644 --- a/src/utils/sizify.js +++ b/src/utils/sizify.js @@ -1,26 +1,25 @@ function sizify(bytes, addI) { - if (bytes == 0) return "0"; + if (bytes === 0) return "0"; if (bytes < 0) bytes = -bytes; + const prefixes = ["", "K", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q"]; - let prefixIndex = Math.floor( - Math.log2 ? Math.log2(bytes) / 10 : Math.log(bytes) / (Math.log(2) * 10), + const prefixIndex = Math.min( + Math.floor(Math.log2(bytes) / 10), + prefixes.length - 1, ); - if (prefixIndex >= prefixes.length - 1) prefixIndex = prefixes.length - 1; - let prefixIndexTranslated = Math.pow(2, 10 * prefixIndex); - let decimalPoints = - 2 - - Math.floor( - Math.log10 - ? Math.log10(bytes / prefixIndexTranslated) - : Math.log(bytes / prefixIndexTranslated) / Math.log(10), - ); - if (decimalPoints < 0) decimalPoints = 0; - return ( + const prefixIndexTranslated = Math.pow(2, 10 * prefixIndex); + const decimalPoints = Math.max( + 2 - Math.floor(Math.log10(bytes / prefixIndexTranslated)), + 0, + ); + + const size = Math.ceil((bytes / prefixIndexTranslated) * Math.pow(10, decimalPoints)) / - Math.pow(10, decimalPoints) + - prefixes[prefixIndex] + - (prefixIndex > 0 && addI ? "i" : "") - ); + Math.pow(10, decimalPoints); + const prefix = prefixes[prefixIndex]; + const suffix = prefixIndex > 0 && addI ? "i" : ""; + + return size + prefix + suffix; } module.exports = sizify; diff --git a/src/utils/urlMojibakeFixer.js b/src/utils/urlMojibakeFixer.js index f7e6c47..7fd718c 100644 --- a/src/utils/urlMojibakeFixer.js +++ b/src/utils/urlMojibakeFixer.js @@ -1,21 +1,16 @@ // Node.JS mojibake URL fixing function function fixNodeMojibakeURL(string) { - var encoded = ""; - - //Encode URLs - Buffer.from(string, "latin1").forEach((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, (match) => { - return match.toUpperCase(); - }); + return Buffer.from(string, "latin1") + .reduce((result, value) => { + if (value > 127) { + result += + "%" + (value < 16 ? "0" : "") + value.toString(16).toUpperCase(); + } else { + result += String.fromCharCode(value); + } + return result; + }, "") + .replace(/%[0-9a-f]{2}/gi, (match) => match.toUpperCase()); } module.exports = fixNodeMojibakeURL; diff --git a/src/utils/urlParser.js b/src/utils/urlParser.js index 1aed5b0..cdc4400 100644 --- a/src/utils/urlParser.js +++ b/src/utils/urlParser.js @@ -1,6 +1,6 @@ const url = require("url"); -// SVR.JS URL parser function (needed only for SVR.JS 2.x and 3.x mods) +// SVR.JS URL parser function (compatible with legacy Node.JS URL parsing function) function parseURL(uri, prepend) { // Replace newline characters with its respective URL encodings uri = uri.replace(/\r/g, "%0D").replace(/\n/g, "%0A"); @@ -13,7 +13,7 @@ function parseURL(uri, prepend) { // Parse the URL using regular expression let parsedURI = uri.match( - /^(?:([^:]+:)(\/\/)?)?(?:([^@/?#*]+)@)?([^:/?#*]+|\[[^*]\/]\])?(?::([0-9]+))?(\*|\/[^?#]*)?(\?[^#]*)?(#[\S\s]*)?/, + /^(?:([^:]+:)(\/\/)?)?(?:([^@/?#*]+)@)?([^:/?#*]+|\[[^*\]/]\])?(?::([0-9]+))?(\*|\/[^?#]*)?(\?[^#]*)?(#[\S\s]*)?/, ); // Match 1: protocol // Match 2: slashes after protocol diff --git a/src/utils/urlSanitizer.js b/src/utils/urlSanitizer.js index 904e2de..b2ada45 100644 --- a/src/utils/urlSanitizer.js +++ b/src/utils/urlSanitizer.js @@ -1,43 +1,50 @@ // SVR.JS path sanitizer function function sanitizeURL(resource, allowDoubleSlashes) { - if (resource == "*" || resource == "") return resource; + if (resource === "*" || resource === "") return resource; + // Remove null characters resource = resource.replace(/%00|\0/g, ""); + // Check if URL is malformed (e.g. %c0%af or %u002f or simply %as) if (resource.match(/%(?:c[01]|f[ef]|(?![0-9a-f]{2}).{2}|.{0,1}$)/i)) throw new URIError("URI malformed"); + // Decode URL-encoded characters while preserving certain characters resource = resource.replace(/%([0-9a-f]{2})/gi, (match, hex) => { const decodedChar = String.fromCharCode(parseInt(hex, 16)); - return /(?!["<>^`{|}?#%])[!-~]/.test(decodedChar) ? decodedChar : "%" + hex; + return /[!$&-;=@-\]_a-z~]/.test(decodedChar) ? decodedChar : match; }); + // Encode certain characters - resource = resource.replace(/[<>^`{|}]]/g, (character) => { + resource = resource.replace(/[<>^`{|}]/g, (character) => { const charCode = character.charCodeAt(0); return ( "%" + (charCode < 16 ? "0" : "") + charCode.toString(16).toUpperCase() ); }); - let sanitizedResource = resource; + // Ensure the resource starts with a slash - if (resource[0] != "/") sanitizedResource = "/" + sanitizedResource; + if (resource[0] !== "/") resource = "/" + resource; + // Convert backslashes to slashes and handle duplicate slashes - sanitizedResource = sanitizedResource + resource = resource .replace(/\\/g, "/") .replace(allowDoubleSlashes ? /\/{3,}/g : /\/+/g, "/"); - // Handle relative navigation (e.g., "/./", "/../", "../", "./"), also remove trailing dots in paths - sanitizedResource = sanitizedResource + + // Handle relative navigation (e.g., "/./", "/../", "../", "./") and remove trailing dots in paths + resource = resource .replace(/\/\.(?:\.{2,})?(?=\/|$)/g, "") .replace(/([^./])\.+(?=\/|$)/g, "$1"); - while (sanitizedResource.match(/\/(?!\.\.\/)[^/]+\/\.\.(?=\/|$)/)) { - sanitizedResource = sanitizedResource.replace( - /\/(?!\.\.\/)[^/]+\/\.\.(?=\/|$)/g, - "", - ); + + // Remove remaining "../" + while (resource.match(/\/(?!\.\.\/)[^/]+\/\.\.(?=\/|$)/)) { + resource = resource.replace(/\/(?!\.\.\/)[^/]+\/\.\.(?=\/|$)/g, ""); } - sanitizedResource = sanitizedResource.replace(/\/\.\.(?=\/|$)/g, ""); - if (sanitizedResource.length == 0) return "/"; - else return sanitizedResource; + resource = resource.replace(/\/\.\.(?=\/|$)/g, ""); + + // If the result has length of 0, return "/", else return the sanitized URL + if (resource.length == 0) return "/"; + else return resource; } module.exports = sanitizeURL; diff --git a/svrjs.json b/svrjs.json index f9477da..139dafd 100644 --- a/svrjs.json +++ b/svrjs.json @@ -1,9 +1,11 @@ { - "version": "4.0.0-beta1", + "version": "4.0.0-beta2", "name": "SVR.JS", "documentationURL": "https://svrjs.org/docs/tentative", "statisticsServerCollectEndpoint": "https://statistics.svrjs.org/collect.svr", "changes": [ - "First SVR.JS 4.0.0 beta version" + "Fixed the bug with \"ext\" variable for .tar.gz mods.", + "Fixed the regular expression in the URL parser.", + "Optimized many functions" ] }