diff --git a/index.html b/index.html index 377da72..bb518d8 100644 --- a/index.html +++ b/index.html @@ -1,25 +1,89 @@ - SVR.JS 3.14.15 + SVR.JS 3.15.0 -

Welcome to SVR.JS 3.14.15

-
- -
+

Welcome to SVR.JS 3.15.0

+
+ +

If you see this page that means that the server is working properly. You can further configure the server and replace index.html and tests.html pages with custom ones.

Default config.json looks like this:

- -
{
+    
+      
{
   "users": [],
   "port": 80,
   "pubport": 80,
@@ -84,13 +148,16 @@
     
     

Changes:

    -
  • Fixed crashes related to the request ID generation.
  • -
  • Optimized HTTP compression functionality.
  • +
  • Changed URL parser from wrapper over WHATWG URL parser to custom regex-based URL parser.
  • +
  • Optimized server code.
  • +
  • Redesigned default error pages.
  • +
  • Removed blocking file system calls from the directory listing function.
  • +
  • Replaced path.extname() function with regex-based function.

- Tests
- Licenses
- SVR.JS status page
+ Tests
+ Licenses
+ SVR.JS status page
SVR.JS documentation

diff --git a/licenses/index.html b/licenses/index.html index 0fde7ce..9815f1c 100644 --- a/licenses/index.html +++ b/licenses/index.html @@ -1,19 +1,83 @@ - SVR.JS 3.14.15 Licenses + SVR.JS 3.15.0 Licenses -

SVR.JS 3.14.15 Licenses

-

SVR.JS 3.14.15

+

SVR.JS 3.15.0 Licenses

+

SVR.JS 3.15.0

MIT License

@@ -37,9 +101,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-

Packages used by SVR.JS 3.14.15

+

Packages used by SVR.JS 3.15.0

-
+
License: MIT
asap @@ -48,7 +112,7 @@ High-priority task queue for Node.js and browsers
-
+
License: MIT
asn1.js (by Fedor Indutny) @@ -57,7 +121,7 @@ ASN.1 encoder and decoder
-
+
License: MIT
asn1.js-rfc2560 (by Fedor Indutny) @@ -66,7 +130,7 @@ RFC2560 structures for asn1.js
-
+
License: MIT
asn1.js-rfc5280 (by Felix Hanley) @@ -75,7 +139,7 @@ RFC5280 extension structures for asn1.js
-
+
License: MIT
async (by Caolan McMahon) @@ -84,7 +148,7 @@ Higher-order functions and common patterns for asynchronous code
-
+
License: MIT
bn.js (by Fedor Indutny) @@ -93,7 +157,7 @@ Big number implementation in pure javascript
-
+
License: MIT
call-bind (by Jordan Harband) @@ -102,7 +166,7 @@ Robustly `.call.bind()` a function
-
+
License: ISC
chownr (by Isaac Z. Schlueter) @@ -111,7 +175,7 @@ like `chown -R`
-
+
License: ISC
dezalgo (by Isaac Z. Schlueter) @@ -120,7 +184,7 @@ Contain async insanity so that the dark pony lord doesn't eat souls
-
+
License: MIT
formidable @@ -130,7 +194,7 @@ Required by SVR.JS
-
+
License: ISC
fs-minipass (by Isaac Z. Schlueter) @@ -140,7 +204,7 @@ Patched to work with Bun
-
+
License: MIT
function-bind (by Raynos) @@ -149,7 +213,7 @@ Implementation of Function.prototype.bind
-
+
License: MIT
get-intrinsic (by Jordan Harband) @@ -158,7 +222,7 @@ Get and robustly cache all JS language-level intrinsics at first require time
-
+
License: ISC
graceful-fs @@ -168,7 +232,7 @@ Required by SVR.JS.
-
+
License: MIT
has (by Thiago de Arruda) @@ -177,7 +241,7 @@ Object.prototype.hasOwnProperty.call shortcut
-
+
License: MIT
has-symbols (by Jordan Harband) @@ -186,7 +250,7 @@ Determine if the JS environment has Symbol support. Supports spec, or shams.
-
+
License: MIT
hexoid (by Luke Edwards) @@ -195,7 +259,7 @@ A tiny (190B) and extremely fast utility to generate random IDs of fixed length
-
+
License: ISC
inherits @@ -204,7 +268,7 @@ Browser-friendly inheritance fully compatible with standard node.js inherits()
-
+
License: MIT
mime-db @@ -213,7 +277,7 @@ Media Type Database
-
+
License: MIT
mime-types @@ -223,7 +287,7 @@ Required by SVR.JS.
-
+
License: ISC
minimalistic-assert @@ -232,7 +296,7 @@ minimalistic-assert ===
-
+
License: ISC
minipass (by Isaac Z. Schlueter) @@ -241,7 +305,7 @@ minimal implementation of a PassThrough stream
-
+
License: MIT
minizlib (by Isaac Z. Schlueter) @@ -250,7 +314,7 @@ A small fast zlib stream built on minipass and Node.js's zlib binding.
-
+
License: MIT
mkdirp @@ -259,7 +323,7 @@ Recursively mkdir, like `mkdir -p`
-
+
License: MIT
object-inspect (by James Halliday) @@ -268,7 +332,7 @@ string representations of objects in node and the browser
-
+
License: MIT
ocsp (by Fedor Indutny) @@ -278,7 +342,7 @@ Required by SVR.JS.
-
+
License: ISC
once (by Isaac Z. Schlueter) @@ -287,7 +351,7 @@ Run a function exactly one time
-
+
License: BSD-3
qs @@ -296,7 +360,7 @@ A querystring parser that supports nesting and arrays, with a depth limit
-
+
License: MIT
side-channel (by Jordan Harband) @@ -305,7 +369,7 @@ Store information about any JS value in a side channel. Uses WeakMap if available.
-
+
License: MIT
simple-lru-cache (by Gabriel Eisbruch) @@ -314,7 +378,7 @@ node-simple-lru-cache =====================
-
+
License: ISC
tar (by Isaac Z. Schlueter) @@ -324,7 +388,7 @@ Required by SVR.JS.
-
+
License: ISC
wrappy (by Isaac Z. Schlueter) @@ -333,7 +397,7 @@ Callback wrapping utility
-
+
License: ISC
yallist (by Isaac Z. Schlueter) diff --git a/svr.js b/svr.js index 0fc81a4..da9912e 100644 --- a/svr.js +++ b/svr.js @@ -69,7 +69,7 @@ function deleteFolderRecursive(path) { } var os = require("os"); -var version = "3.14.15"; +var version = "3.15.0"; var singlethreaded = false; if (process.versions) process.versions.svrjs = version; // Inject SVR.JS into process.versions @@ -392,7 +392,7 @@ try { } var inspector = undefined; try { - inspector = require("inspector"); + inspector = require("inspector"); } catch (err) { // Don't use inspector } @@ -1347,6 +1347,81 @@ function sanitizeURL(resource, allowDoubleSlashes) { else return sanitizedResource; } +// 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 + var hasSlashes = (uri.indexOf("/") != -1); + + // Parse the URL using regular expression + var 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 + var 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 + var qobject = Object.create(null); + var parsedQuery = parsedURI[7].substring(1).match(/([^&=]*)(?:=([^&]*))?/g); + parsedQuery.forEach(function (qp) { + if (qp.length > 0) { + var 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.path ? uobject.path : "") + (uobject.hash ? uobject.hash : ""); + + return uobject; +} + // Node.JS mojibake URL fixing function function fixNodeMojibakeURL(string) { var encoded = ""; @@ -2548,7 +2623,7 @@ if (!cluster.isPrimary) { if (err.code == "ERR_SSL_HTTP_REQUEST" && process.version && parseInt(process.version.split(".")[0].substring(1)) >= 16) { // Disable custom error page for HTTP SSL error res.writeHead(errorCode, http.STATUS_CODES[errorCode], cheaders); - res.write(("{errorMessage}

{errorMessage}

{errorDesc}

{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(/{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, ">")).replace(/{contact}/g, serverAdmin.replace(/&/g, "&").replace(//g, ">").replace(/\./g, "[dot]").replace(/@/g, "[at]"))); + res.write(("{errorMessage}

{errorMessage}

{errorDesc}

{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(/{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, ">")).replace(/{contact}/g, serverAdmin.replace(/&/g, "&").replace(//g, ">").replace(/\./g, "[dot]").replace(/@/g, "[at]"))); res.end(); } else { fs.readFile(errorFile, function (err, data) { @@ -2572,7 +2647,7 @@ if (!cluster.isPrimary) { 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(/{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, ">")).replace(/{contact}/g, serverAdmin.replace(/&/g, "&").replace(//g, ">").replace(/\./g, "[dot]").replace(/@/g, "[at]")).replace(/{additionalError}/g, additionalError.toString())); + 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(/{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, ">")).replace(/{contact}/g, serverAdmin.replace(/&/g, "&").replace(//g, ">").replace(/\./g, "[dot]").replace(/@/g, "[at]")).replace(/{additionalError}/g, additionalError.toString())); res.end(); } }); @@ -2790,13 +2865,11 @@ if (!cluster.isPrimary) { modFunction(); } - function vres(req, socket, head, serverconsole) { - return function () { - serverconsole.errmessage("SVR.JS doesn't support proxy without proxy mod."); - if (!socket.destroyed) socket.end("HTTP/1.1 501 Not Implemented\n\n"); - }; + function vres() { + serverconsole.errmessage("SVR.JS doesn't support proxy without proxy mod."); + if (!socket.destroyed) socket.end("HTTP/1.1 501 Not Implemented\n\n"); } - modExecute(mods, vres(req, socket, head, serverconsole)); + modExecute(mods, vres); } function reqhandler(req, res, fromMain) { @@ -3232,7 +3305,7 @@ if (!cluster.isPrimary) { } 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.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(); } }); @@ -3247,6 +3320,7 @@ if (!cluster.isPrimary) { callServerError(500, err); } + // Function to perform HTTP redirection to a specified destination URL function redirect(destination, isTemporary, keepMethod, customHeaders) { // If keepMethod is a object, then save it to customHeaders @@ -3318,72 +3392,24 @@ if (!cluster.isPrimary) { }); } - - // Function to parse a URL string into a URL object - function parseURL(uri) { - // Prepare the path (remove multiple slashes) - var preparedURI = uri.replace(/^\/{2,}/,"/"); - // Check if the URL API is available (Node.js version >= 10) - if (typeof URL !== "undefined" && url.Url) { - try { - // Create a new URL object using the provided URI and base URL - var uobject = new URL(preparedURI, "http" + (req.socket.encrypted ? "s" : "") + "://" + (req.headers.host ? req.headers.host : (domain ? domain : "unknown.invalid"))); - - // Create a new URL object (similar to deprecated url.Url) - var nuobject = new url.Url(); - - // Set properties of the new URL object from the provided URL - if (preparedURI.indexOf("/") != -1) nuobject.slashes = true; - if (uobject.protocol != "") nuobject.protocol = uobject.protocol; - if (uobject.username != "" && uobject.password != "") nuobject.auth = uobject.username + ":" + uobject.password; - if (uobject.host != "") nuobject.host = uobject.host; - if (uobject.hostname != "") nuobject.hostname = uobject.hostname; - if (uobject.port != "") nuobject.port = uobject.port; - if (uobject.pathname != "") nuobject.pathname = uobject.pathname; - if (uobject.search != "") nuobject.search = uobject.search; - if (uobject.hash != "") nuobject.hash = uobject.hash; - if (uobject.href != "") nuobject.href = uobject.href; - - // Adjust the pathname and href properties if the URI doesn't start with "/" - if (preparedURI[0] != "/") { - if (nuobject.pathname) { - nuobject.pathname = nuobject.pathname.substring(1); - nuobject.href = nuobject.pathname + (nuobject.search ? nuobject.search : ""); - } - } - - // Set the path property as a combination of pathname and search - if (nuobject.pathname) { - nuobject.path = nuobject.pathname + (nuobject.search ? nuobject.search : ""); - } - - // Initialize the query object and copy URL search parameters to it - nuobject.query = {}; - uobject.searchParams.forEach(function (value, key) { - nuobject.query[key] = value; - }); - - // Return the created URL object - return nuobject; - } catch (err) { - // If there was an error using the URL API, fall back to deprecated url.parse - return url.parse(preparedURI, true); - } - } else { - // If the URL API is not available, fall back to deprecated url.parse - return url.parse(preparedURI, true); - } - } - // Authenticated user variable var authUser = null; // URL-related objects. - var uobject = parseURL(req.url); + var uobject = {}; + try { + uobject = parseURL(req.url, "http" + (req.socket.encrypted ? "s" : "") + "://" + (req.headers.host ? req.headers.host : (domain ? domain : "unknown.invalid"))); + } catch (err) { + // Return an 400 error + callServerError(400); + serverconsole.errmessage("Bad request!"); + return; + } var search = uobject.search; var href = uobject.pathname; - var ext = path.extname(href).toLowerCase(); - ext = ext.substring(1, ext.length + 1); + var ext = href.match(/[^\/]\.([^.]+)$/); + if(!ext) ext = ""; + else ext = ext[1].toLowerCase(); var decodedHref = ""; try { decodedHref = decodeURIComponent(href); @@ -3393,6 +3419,7 @@ if (!cluster.isPrimary) { serverconsole.errmessage("Bad request!"); return; } + var origHref = href; // Placeholder origHref if (req.headers["expect"] && req.headers["expect"] != "100-continue") { // Expectations not met. @@ -3424,357 +3451,247 @@ if (!cluster.isPrimary) { var vresCalled = false; - function vres(req, res, serverconsole, responseEnd, href, ext, uobject, search, defaultpage, users, page404, head, foot, fd, callServerError, getCustomHeaders, origHref, redirect, parsePostData, authUser) { - return function () { - if (vresCalled) { - process.emitWarning("elseCallback() invoked multiple times.", { - code: "WARN_SVRJS_MULTIPLE_ELSECALLBACK" - }); - return; - } else { - vresCalled = true; - } + function vres() { + if (vresCalled) { + process.emitWarning("elseCallback() invoked multiple times.", { + code: "WARN_SVRJS_MULTIPLE_ELSECALLBACK" + }); + return; + } else { + vresCalled = true; + } - // Function to check the level of a path relative to the web root - function checkPathLevel(path) { - // Split the path into an array of components based on "/" - var pathComponents = path.split("/"); + // Function to check the level of a path relative to the web root + function checkPathLevel(path) { + // Split the path into an array of components based on "/" + var pathComponents = path.split("/"); - // Initialize counters for level up (..) and level down (.) - var levelUpCount = 0; - var levelDownCount = 0; + // Initialize counters for level up (..) and level down (.) + var levelUpCount = 0; + var levelDownCount = 0; - // Loop through the path components - for (var i = 0; i < pathComponents.length; i++) { - // If the component is "..", decrement the levelUpCount - if (".." === pathComponents[i]) { - levelUpCount--; - } - // If the component is not "." or an empty string, increment the levelDownCount - else if ("." !== pathComponents[i] && "" !== pathComponents[i]) { - levelDownCount++; - } + // Loop through the path components + for (var i = 0; i < pathComponents.length; i++) { + // If the component is "..", decrement the levelUpCount + if (".." === pathComponents[i]) { + levelUpCount--; } - - // Calculate the overall level by subtracting levelUpCount from levelDownCount - var overallLevel = levelDownCount - levelUpCount; - - // Return the overall level - return overallLevel; - } - - - if (isProxy) { - var eheaders = getCustomHeaders(); - eheaders["Content-Type"] = "text/html; charset=utf-8"; - res.writeHead(501, http.STATUS_CODES[501], eheaders); - res.write("Proxy not implemented

Proxy not implemented

SVR.JS doesn't support proxy without proxy mod. If you're administator of this server, then install this mod in order to use SVR.JS as a proxy.

" + (exposeServerVersion ? "SVR.JS/" + version + " (" + getOS() + "; " + (process.isBun ? ("Bun/v" + process.versions.bun + "; like Node.JS/" + process.version) : ("Node.JS/" + process.version)) + ")" : "SVR.JS").replace(/&/g, "&").replace(//g, ">") + "

"); - res.end(); - serverconsole.errmessage("SVR.JS doesn't support proxy without proxy mod."); - return; - } - - if (req.method == "OPTIONS") { - var hdss = getCustomHeaders(); - hdss["Allow"] = "GET, POST, HEAD, OPTIONS"; - res.writeHead(204, http.STATUS_CODES[204], hdss); - res.end(); - return; - } else if (req.method != "GET" && req.method != "POST" && req.method != "HEAD") { - callServerError(405); - serverconsole.errmessage("Invaild method: " + req.method); - return; - } - - if (allowStatus && (href == "/svrjsstatus.svr" || (os.platform() == "win32" && href.toLowerCase() == "/svrjsstatus.svr"))) { - function formatRelativeTime(relativeTime) { - var days = Math.floor(relativeTime / 60 / (60 * 24)); - var dateDiff = new Date(relativeTime * 1000); - return days + " days, " + dateDiff.getUTCHours() + " hours, " + dateDiff.getUTCMinutes() + " minutes, " + dateDiff.getUTCSeconds() + " seconds"; + // If the component is not "." or an empty string, increment the levelDownCount + else if ("." !== pathComponents[i] && "" !== pathComponents[i]) { + levelDownCount++; } - var statusBody = ""; - statusBody += "Server version: " + (exposeServerVersion ? "SVR.JS/" + version + " (" + getOS() + "; " + (process.isBun ? ("Bun/v" + process.versions.bun + "; like Node.JS/" + process.version) : ("Node.JS/" + process.version)) + ")" : "SVR.JS").replace(/&/g, "&").replace(//g, ">") + "

"; - - //Those entries are just dates and numbers converted/formatted to strings, so no escaping is needed. - statusBody += "Current time: " + new Date().toString() + "
Thread start time: " + new Date(new Date() - (process.uptime() * 1000)).toString() + "
Thread uptime: " + formatRelativeTime(Math.floor(process.uptime())) + "
"; - statusBody += "OS uptime: " + formatRelativeTime(os.uptime()) + "
"; - statusBody += "Total request count: " + reqcounter + "
"; - statusBody += "Average request rate: " + (Math.round((reqcounter / process.uptime()) * 100) / 100) + " requests/s
"; - statusBody += "Client errors (4xx): " + err4xxcounter + "
"; - statusBody += "Server errors (5xx): " + err5xxcounter + "
"; - statusBody += "Average error rate: " + (Math.round(((err4xxcounter + err5xxcounter) / reqcounter) * 10000) / 100) + "%
"; - statusBody += "Malformed HTTP requests: " + malformedcounter; - if (process.memoryUsage) statusBody += "
Memory usage of thread: " + sizify(process.memoryUsage().rss, true) + "B"; - if (process.cpuUsage) statusBody += "
Total CPU usage by thread: u" + (process.cpuUsage().user / 1000) + "ms s" + (process.cpuUsage().system / 1000) + "ms - " + (Math.round((((process.cpuUsage().user + process.cpuUsage().system) / 1000000) / process.uptime()) * 1000) / 1000) + "%"; - statusBody += "
Thread PID: " + process.pid + "
"; - - var hdhds = getCustomHeaders(); - hdhds["Content-Type"] = "text/html; charset=utf-8"; - res.writeHead(200, http.STATUS_CODES[200], hdhds); - res.end((head == "" ? "SVR.JS status" + (req.headers.host == undefined ? "" : " for " + String(req.headers.host).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")) + "" : head.replace(//i, "SVR.JS status" + (req.headers.host == undefined ? "" : " for " + String(req.headers.host).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")) + "")) + "

SVR.JS status" + (req.headers.host == undefined ? "" : " for " + String(req.headers.host).replace(/&/g, "&").replace(//g, ">")) + "

" + statusBody + (foot == "" ? "" : foot)); - return; } - var pth = decodeURIComponent(href).replace(/\/+/g, "/").substring(1); - var readFrom = "./" + pth; - var dirImagesMissing = false; - fs.stat(readFrom, function (err, stats) { - if (err) { - if (err.code == "ENOENT") { - if (__dirname != process.cwd() && pth.match(/^\.dirimages\/(?:(?!\.png$).)+\.png$/)) { - dirImagesMissing = true; - readFrom = __dirname + "/" + pth; - } else { - callServerError(404); - serverconsole.errmessage("Resource not found."); - return; - } - } else if (err.code == "ENOTDIR") { - callServerError(404); // Assume that file doesn't exist. + // Calculate the overall level by subtracting levelUpCount from levelDownCount + var overallLevel = levelDownCount - levelUpCount; + + // Return the overall level + return overallLevel; + } + + + if (isProxy) { + var eheaders = getCustomHeaders(); + eheaders["Content-Type"] = "text/html; charset=utf-8"; + res.writeHead(501, http.STATUS_CODES[501], eheaders); + res.write("Proxy not implemented

Proxy not implemented

SVR.JS doesn't support proxy without proxy mod. If you're administator of this server, then install this mod in order to use SVR.JS as a proxy.

" + (exposeServerVersion ? "SVR.JS/" + version + " (" + getOS() + "; " + (process.isBun ? ("Bun/v" + process.versions.bun + "; like Node.JS/" + process.version) : ("Node.JS/" + process.version)) + ")" : "SVR.JS").replace(/&/g, "&").replace(//g, ">") + "

"); + res.end(); + serverconsole.errmessage("SVR.JS doesn't support proxy without proxy mod."); + return; + } + + if (req.method == "OPTIONS") { + var hdss = getCustomHeaders(); + hdss["Allow"] = "GET, POST, HEAD, OPTIONS"; + res.writeHead(204, http.STATUS_CODES[204], hdss); + res.end(); + return; + } else if (req.method != "GET" && req.method != "POST" && req.method != "HEAD") { + callServerError(405); + serverconsole.errmessage("Invaild method: " + req.method); + return; + } + + if (allowStatus && (href == "/svrjsstatus.svr" || (os.platform() == "win32" && href.toLowerCase() == "/svrjsstatus.svr"))) { + function formatRelativeTime(relativeTime) { + var days = Math.floor(relativeTime / 60 / (60 * 24)); + var dateDiff = new Date(relativeTime * 1000); + return days + " days, " + dateDiff.getUTCHours() + " hours, " + dateDiff.getUTCMinutes() + " minutes, " + dateDiff.getUTCSeconds() + " seconds"; + } + var statusBody = ""; + statusBody += "Server version: " + (exposeServerVersion ? "SVR.JS/" + version + " (" + getOS() + "; " + (process.isBun ? ("Bun/v" + process.versions.bun + "; like Node.JS/" + process.version) : ("Node.JS/" + process.version)) + ")" : "SVR.JS").replace(/&/g, "&").replace(//g, ">") + "

"; + + //Those entries are just dates and numbers converted/formatted to strings, so no escaping is needed. + statusBody += "Current time: " + new Date().toString() + "
Thread start time: " + new Date(new Date() - (process.uptime() * 1000)).toString() + "
Thread uptime: " + formatRelativeTime(Math.floor(process.uptime())) + "
"; + statusBody += "OS uptime: " + formatRelativeTime(os.uptime()) + "
"; + statusBody += "Total request count: " + reqcounter + "
"; + statusBody += "Average request rate: " + (Math.round((reqcounter / process.uptime()) * 100) / 100) + " requests/s
"; + statusBody += "Client errors (4xx): " + err4xxcounter + "
"; + statusBody += "Server errors (5xx): " + err5xxcounter + "
"; + statusBody += "Average error rate: " + (Math.round(((err4xxcounter + err5xxcounter) / reqcounter) * 10000) / 100) + "%
"; + statusBody += "Malformed HTTP requests: " + malformedcounter; + if (process.memoryUsage) statusBody += "
Memory usage of thread: " + sizify(process.memoryUsage().rss, true) + "B"; + if (process.cpuUsage) statusBody += "
Total CPU usage by thread: u" + (process.cpuUsage().user / 1000) + "ms s" + (process.cpuUsage().system / 1000) + "ms - " + (Math.round((((process.cpuUsage().user + process.cpuUsage().system) / 1000000) / process.uptime()) * 1000) / 1000) + "%"; + statusBody += "
Thread PID: " + process.pid + "
"; + + res.writeHead(200, http.STATUS_CODES[200], { + "Content-Type": "text/html; charset=utf-8" + }); + res.end((head == "" ? "SVR.JS status" + (req.headers.host == undefined ? "" : " for " + String(req.headers.host).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")) + "" : head.replace(//i, "SVR.JS status" + (req.headers.host == undefined ? "" : " for " + String(req.headers.host).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")) + "")) + "

SVR.JS status" + (req.headers.host == undefined ? "" : " for " + String(req.headers.host).replace(/&/g, "&").replace(//g, ">")) + "

" + statusBody + (foot == "" ? "" : foot)); + return; + } + + var dHref = decodeURIComponent(href); + var readFrom = "." + dHref; + var dirImagesMissing = false; + fs.stat(readFrom, function (err, stats) { + if (err) { + if (err.code == "ENOENT") { + if (__dirname != process.cwd() && dHref.match(/^\/\.dirimages\/(?:(?!\.png$).)+\.png$/)) { + dirImagesMissing = true; + readFrom = __dirname + dHref; + } else { + callServerError(404); serverconsole.errmessage("Resource not found."); return; - } else if (err.code == "EACCES") { - callServerError(403); - serverconsole.errmessage("Access denied."); - return; - } else if (err.code == "ENAMETOOLONG") { - callServerError(414); - return; - } else if (err.code == "EMFILE") { - callServerError(503); - return; - } else if (err.code == "ELOOP") { - callServerError(508); // The symbolic link loop is detected during file system operations. - serverconsole.errmessage("Symbolic link loop detected."); - return; - } else { - callServerError(500, err); - return; } - } - - // Check if index file exists - if (!dirImagesMissing && (req.url == "/" || stats.isDirectory())) { - fs.stat((readFrom + "/index.html").replace(/\/+/g, "/"), function (e, s) { - if (e || !s.isFile()) { - fs.stat((readFrom + "/index.htm").replace(/\/+/g, "/"), function (e, s) { - if (e || !s.isFile()) { - fs.stat((readFrom + "/index.xhtml").replace(/\/+/g, "/"), function (e, s) { - if (e || !s.isFile()) { - properDirectoryListingAndStaticFileServe(); - } else { - stats = s; - pth = (pth + "/index.xhtml").replace(/\/+/g, "/"); - ext = "xhtml"; - readFrom = "./" + pth; - properDirectoryListingAndStaticFileServe(); - } - }); - } else { - stats = s; - pth = (pth + "/index.htm").replace(/\/+/g, "/"); - ext = "htm"; - readFrom = "./" + pth; - properDirectoryListingAndStaticFileServe(); - } - }); - } else { - stats = s; - pth = (pth + "/index.html").replace(/\/+/g, "/"); - ext = "html"; - readFrom = "./" + pth; - properDirectoryListingAndStaticFileServe(); - } - }); - } else if (dirImagesMissing) { - fs.stat(readFrom, function (e, s) { - if (e || !s.isFile()) { - properDirectoryListingAndStaticFileServe(); - } else { - stats = s; - properDirectoryListingAndStaticFileServe(); - } - }); + } else if (err.code == "ENOTDIR") { + callServerError(404); // Assume that file doesn't exist. + serverconsole.errmessage("Resource not found."); + return; + } else if (err.code == "EACCES") { + callServerError(403); + serverconsole.errmessage("Access denied."); + return; + } else if (err.code == "ENAMETOOLONG") { + callServerError(414); + return; + } else if (err.code == "EMFILE") { + callServerError(503); + return; + } else if (err.code == "ELOOP") { + callServerError(508); // The symbolic link loop is detected during file system operations. + serverconsole.errmessage("Symbolic link loop detected."); + return; } else { - properDirectoryListingAndStaticFileServe(); + callServerError(500, err); + return; } + } - function properDirectoryListingAndStaticFileServe() { - if (stats.isFile()) { - var acceptEncoding = req.headers["accept-encoding"]; - if (!acceptEncoding) acceptEncoding = ""; - - var filelen = stats.size; - - // ETag code - var fileETag = undefined; - if (configJSON.enableETag == undefined || configJSON.enableETag) { - fileETag = generateETag(href, stats); - // Check if the client's request matches the ETag value (If-None-Match) - var clientETag = req.headers["if-none-match"]; - if (clientETag === fileETag) { - var headers = getCustomHeaders(); - headers.ETag = clientETag; - res.writeHead(304, http.STATUS_CODES[304], headers); - res.end(); - return; + // Check if index file exists + if (!dirImagesMissing && (req.url == "/" || stats.isDirectory())) { + fs.stat((readFrom + "/index.html").replace(/\/+/g, "/"), function (e, s) { + if (e || !s.isFile()) { + fs.stat((readFrom + "/index.htm").replace(/\/+/g, "/"), function (e, s) { + if (e || !s.isFile()) { + fs.stat((readFrom + "/index.xhtml").replace(/\/+/g, "/"), function (e, s) { + if (e || !s.isFile()) { + properDirectoryListingAndStaticFileServe(); + } else { + stats = s; + ext = "xhtml"; + readFrom = (readFrom + "/index.xhtml").replace(/\/+/g, "/"); + properDirectoryListingAndStaticFileServe(); + } + }); + } else { + stats = s; + ext = "htm"; + readFrom = (readFrom + "/index.htm").replace(/\/+/g, "/"); + properDirectoryListingAndStaticFileServe(); } + }); + } else { + stats = s; + ext = "html"; + readFrom = (readFrom + "/index.html").replace(/\/+/g, "/"); + properDirectoryListingAndStaticFileServe(); + } + }); + } else if (dirImagesMissing) { + fs.stat(readFrom, function (e, s) { + if (e || !s.isFile()) { + properDirectoryListingAndStaticFileServe(); + } else { + stats = s; + properDirectoryListingAndStaticFileServe(); + } + }); + } else { + properDirectoryListingAndStaticFileServe(); + } - // Check if the client's request doesn't match the ETag value (If-Match) - var ifMatchETag = req.headers["if-match"]; - if (ifMatchETag && ifMatchETag !== "*" && ifMatchETag !== fileETag) { - var headers = getCustomHeaders(); - headers.ETag = clientETag; - callServerError(412, headers); - return; - } + function properDirectoryListingAndStaticFileServe() { + if (stats.isFile()) { + var acceptEncoding = req.headers["accept-encoding"]; + if (!acceptEncoding) acceptEncoding = ""; + + var filelen = stats.size; + + // ETag code + var fileETag = undefined; + if (configJSON.enableETag == undefined || configJSON.enableETag) { + fileETag = generateETag(href, stats); + // Check if the client's request matches the ETag value (If-None-Match) + var clientETag = req.headers["if-none-match"]; + if (clientETag === fileETag) { + res.writeHead(304, http.STATUS_CODES[304], { + "ETag": clientETag + }); + res.end(); + return; } - // Handle partial content request - if (ext != "html" && req.headers["range"]) { - try { - var rhd = getCustomHeaders(); - rhd["Accept-Ranges"] = "bytes"; - rhd["Content-Range"] = "bytes */" + filelen; - var regexmatch = req.headers["range"].match(/bytes=([0-9]*)-([0-9]*)/); - if (!regexmatch) { + // Check if the client's request doesn't match the ETag value (If-Match) + var ifMatchETag = req.headers["if-match"]; + if (ifMatchETag && ifMatchETag !== "*" && ifMatchETag !== fileETag) { + callServerError(412, { + "ETag": clientETag + }); + return; + } + } + + // Handle partial content request + if (ext != "html" && req.headers["range"]) { + try { + var rhd = getCustomHeaders(); + rhd["Accept-Ranges"] = "bytes"; + rhd["Content-Range"] = "bytes */" + filelen; + var regexmatch = req.headers["range"].match(/bytes=([0-9]*)-([0-9]*)/); + if (!regexmatch) { + callServerError(416, rhd); + } else { + // Process the partial content request + var beginOrig = regexmatch[1]; + var endOrig = regexmatch[2]; + var begin = 0; + var end = filelen - 1; + if (beginOrig == "" && endOrig == "") { callServerError(416, rhd); + return; + } else if (beginOrig == "") { + begin = end - parseInt(endOrig) + 1; } else { - // Process the partial content request - var beginOrig = regexmatch[1]; - var endOrig = regexmatch[2]; - var begin = 0; - var end = filelen - 1; - if (beginOrig == "" && endOrig == "") { - callServerError(416, rhd); - return; - } else if (beginOrig == "") { - begin = end - parseInt(endOrig) + 1; - } else { - begin = parseInt(beginOrig); - if (endOrig != "") end = parseInt(endOrig); - } - if (begin > end || begin < 0 || begin > filelen - 1) { - callServerError(416, rhd); - return; - } - if (end > filelen - 1) end = filelen - 1; - rhd["Content-Range"] = "bytes " + begin + "-" + end + "/" + filelen; - rhd["Content-Length"] = end - begin + 1; - if (!(mime.contentType(ext) == false) && ext != "") rhd["Content-Type"] = mime.contentType(ext); - if (fileETag) rhd["ETag"] = fileETag; - - if (req.method != "HEAD") { - var readStream = fs.createReadStream(readFrom, { - start: begin, - end: end - }); - readStream.on("error", function (err) { - if (err.code == "ENOENT") { - callServerError(404); - serverconsole.errmessage("Resource not found."); - } else if (err.code == "ENOTDIR") { - callServerError(404); // Assume that file doesn't exist. - serverconsole.errmessage("Resource not found."); - } else if (err.code == "EACCES") { - callServerError(403); - serverconsole.errmessage("Access denied."); - } else if (err.code == "ENAMETOOLONG") { - callServerError(414); - } else if (err.code == "EMFILE") { - callServerError(503); - } else if (err.code == "ELOOP") { - callServerError(508); // The symbolic link loop is detected during file system operations. - serverconsole.errmessage("Symbolic link loop detected."); - } else { - callServerError(500, err); - } - }).on("open", function () { - try { - res.writeHead(206, http.STATUS_CODES[206], rhd); - readStream.pipe(res); - serverconsole.resmessage("Client successfully received content."); - } catch (err) { - callServerError(500, err); - } - }); - } else { - res.writeHead(206, http.STATUS_CODES[206], rhd); - res.end(); - } + begin = parseInt(beginOrig); + if (endOrig != "") end = parseInt(endOrig); } - } catch (err) { - callServerError(500, err); - } - } else { - // Helper function to check if compression is allowed for the file - function canCompress(path, list) { - var canCompress = true; - for (var i = 0; i < list.length; i++) { - if (createRegex(list[i], true).test(path)) { - canCompress = false; - break; - } + if (begin > end || begin < 0 || begin > filelen - 1) { + callServerError(416, rhd); + return; } - return canCompress; - } - - var useBrotli = (ext != "br" && filelen > 256 && zlib.createBrotliCompress && acceptEncoding.match(/\bbr\b/)); - var useDeflate = (ext != "zip" && filelen > 256 && acceptEncoding.match(/\bdeflate\b/)); - var useGzip = (ext != "gz" && filelen > 256 && acceptEncoding.match(/\bgzip\b/)); - - var isCompressable = true; - try { - // Check for files not to compressed and compression enabling setting. Also check for browser quirks and adjust compression accordingly - if((!useBrotli && !useDeflate && !useGzip) || configJSON.enableCompression !== true || !canCompress(href, dontCompress)) { - isCompressable = false; // Compression is disabled - } else if (ext != "html" && ext != "htm" && ext != "xhtml" && ext != "xht" && ext != "shtml") { - if (/^Mozilla\/4\.[0-9]+(( *\[[^)]*\] *| *)\([^)\]]*\))? *$/.test(req.headers["user-agent"]) && !(/https?:\/\/|[bB][oO][tT]|[sS][pP][iI][dD][eE][rR]|[sS][uU][rR][vV][eE][yY]|MSIE/.test(req.headers["user-agent"]))) { - isCompressable = false; // Netscape 4.x doesn't handle compressed data properly outside of HTML documents. - } else if (/^w3m\/[^ ]*$/.test(req.headers["user-agent"])) { - isCompressable = false; // w3m doesn't handle compressed data properly outside of HTML documents. - } - } else { - if (/^Mozilla\/4\.0[6-8](( *\[[^)]*\] *| *)\([^)\]]*\))? *$/.test(req.headers["user-agent"]) && !(/https?:\/\/|[bB][oO][tT]|[sS][pP][iI][dD][eE][rR]|[sS][uU][rR][vV][eE][yY]|MSIE/.test(req.headers["user-agent"]))) { - isCompressable = false; // Netscape 4.06-4.08 doesn't handle compressed data properly. - } - } - } catch (err) { - callServerError(500, err); - return; - } - - // Bun 1.1 has definition for zlib.createBrotliCompress, but throws an error while invoking the function. - if (process.isBun && useBrotli && isCompressable) { - try { - zlib.createBrotliCompress(); - } catch (err) { - useBrotli = false; - } - } - - try { - var hdhds = getCustomHeaders(); - if (useBrotli && isCompressable) { - hdhds["Content-Encoding"] = "br"; - } else if (useDeflate && isCompressable) { - hdhds["Content-Encoding"] = "deflate"; - } else if (useGzip && isCompressable) { - hdhds["Content-Encoding"] = "gzip"; - } else { - if (ext == "html") { - hdhds["Content-Length"] = head.length + filelen + foot.length; - } else { - hdhds["Content-Length"] = filelen; - } - } - if (ext != "html") hdhds["Accept-Ranges"] = "bytes"; - delete hdhds["Content-Type"]; - if (!(mime.contentType(ext) == false) && ext != "") hdhds["Content-Type"] = mime.contentType(ext); - if (fileETag) hdhds["ETag"] = fileETag; + if (end > filelen - 1) end = filelen - 1; + rhd["Content-Range"] = "bytes " + begin + "-" + end + "/" + filelen; + rhd["Content-Length"] = end - begin + 1; + if (!(mime.contentType(ext) == false) && ext != "") rhd["Content-Type"] = mime.contentType(ext); + if (fileETag) rhd["ETag"] = fileETag; if (req.method != "HEAD") { - var readStream = fs.createReadStream(readFrom); + var readStream = fs.createReadStream(readFrom, { + start: begin, + end: end + }); readStream.on("error", function (err) { if (err.code == "ENOENT") { callServerError(404); @@ -3797,233 +3714,91 @@ if (!cluster.isPrimary) { } }).on("open", function () { try { - var resStream = {}; - if (useBrotli && isCompressable) { - resStream = zlib.createBrotliCompress(); - resStream.pipe(res); - } else if (useDeflate && isCompressable) { - resStream = zlib.createDeflateRaw(); - resStream.pipe(res); - } else if (useGzip && isCompressable) { - resStream = zlib.createGzip(); - resStream.pipe(res); - } else { - resStream = res; - } - if (ext == "html") { - function afterWriteCallback() { - readStream.on("end", function () { - resStream.end(foot); - }); - readStream.pipe(resStream, { - end: false - }); - } - res.writeHead(200, http.STATUS_CODES[200], hdhds); - if (!resStream.write(head)) { - resStream.on("drain", afterWriteCallback); - } else { - process.nextTick(afterWriteCallback); - } - } else { - res.writeHead(200, http.STATUS_CODES[200], hdhds); - readStream.pipe(resStream); - } + res.writeHead(206, http.STATUS_CODES[206], rhd); + readStream.pipe(res); serverconsole.resmessage("Client successfully received content."); } catch (err) { callServerError(500, err); } }); } else { - res.writeHead(200, http.STATUS_CODES[200], hdhds); + res.writeHead(206, http.STATUS_CODES[206], rhd); res.end(); - serverconsole.resmessage("Client successfully received content."); } + } + } catch (err) { + callServerError(500, err); + } + } else { + // Helper function to check if compression is allowed for the file + function canCompress(path, list) { + var canCompress = true; + for (var i = 0; i < list.length; i++) { + if (createRegex(list[i], true).test(path)) { + canCompress = false; + break; + } + } + return canCompress; + } + + var useBrotli = (ext != "br" && filelen > 256 && zlib.createBrotliCompress && acceptEncoding.match(/\bbr\b/)); + var useDeflate = (ext != "zip" && filelen > 256 && acceptEncoding.match(/\bdeflate\b/)); + var useGzip = (ext != "gz" && filelen > 256 && acceptEncoding.match(/\bgzip\b/)); + + var isCompressable = true; + try { + // Check for files not to compressed and compression enabling setting. Also check for browser quirks and adjust compression accordingly + if((!useBrotli && !useDeflate && !useGzip) || configJSON.enableCompression !== true || !canCompress(href, dontCompress)) { + isCompressable = false; // Compression is disabled + } else if (ext != "html" && ext != "htm" && ext != "xhtml" && ext != "xht" && ext != "shtml") { + if (/^Mozilla\/4\.[0-9]+(( *\[[^)]*\] *| *)\([^)\]]*\))? *$/.test(req.headers["user-agent"]) && !(/https?:\/\/|[bB][oO][tT]|[sS][pP][iI][dD][eE][rR]|[sS][uU][rR][vV][eE][yY]|MSIE/.test(req.headers["user-agent"]))) { + isCompressable = false; // Netscape 4.x doesn't handle compressed data properly outside of HTML documents. + } else if (/^w3m\/[^ ]*$/.test(req.headers["user-agent"])) { + isCompressable = false; // w3m doesn't handle compressed data properly outside of HTML documents. + } + } else { + if (/^Mozilla\/4\.0[6-8](( *\[[^)]*\] *| *)\([^)\]]*\))? *$/.test(req.headers["user-agent"]) && !(/https?:\/\/|[bB][oO][tT]|[sS][pP][iI][dD][eE][rR]|[sS][uU][rR][vV][eE][yY]|MSIE/.test(req.headers["user-agent"]))) { + isCompressable = false; // Netscape 4.06-4.08 doesn't handle compressed data properly. + } + } + } catch (err) { + callServerError(500, err); + return; + } + + // Bun 1.1 has definition for zlib.createBrotliCompress, but throws an error while invoking the function. + if (process.isBun && useBrotli && isCompressable) { + try { + zlib.createBrotliCompress(); } catch (err) { - callServerError(500, err); + useBrotli = false; } } - } else if (stats.isDirectory()) { - // Check if directory listing is enabled in the configuration - if (checkForEnabledDirectoryListing(req.headers.host, req.socket ? req.socket.localAddress : undefined)) { - var customHeaders = getCustomHeaders(); - customHeaders["Content-Type"] = "text/html; charset=utf-8"; - res.writeHead(200, http.STATUS_CODES[200], customHeaders); - // Read custom header and footer content (if available) - var customDirListingHeader = fs.existsSync(("." + decodeURIComponent(href) + "/.dirhead").replace(/\/+/g, "/")) ? - fs.readFileSync(("." + decodeURIComponent(href) + "/.dirhead").replace(/\/+/g, "/")).toString() : - (fs.existsSync(("." + decodeURIComponent(href) + "/HEAD.html").replace(/\/+/g, "/")) && (os.platform != "win32" || href != "/")) ? - fs.readFileSync(("." + decodeURIComponent(href) + "/HEAD.html").replace(/\/+/g, "/")).toString() : - ""; - var customDirListingFooter = fs.existsSync(("." + decodeURIComponent(href) + "/.dirfoot").replace(/\/+/g, "/")) ? - fs.readFileSync(("." + decodeURIComponent(href) + "/.dirfoot").replace(/\/+/g, "/")).toString() : - (fs.existsSync(("." + decodeURIComponent(href) + "/FOOT.html").replace(/\/+/g, "/")) && (os.platform != "win32" || href != "/")) ? - fs.readFileSync(("." + decodeURIComponent(href) + "/FOOT.html").replace(/\/+/g, "/")).toString() : - ""; - - // Check if custom header has HTML tag - var headerHasHTMLTag = customDirListingHeader.replace(/|$)/g, "").match(/])*(?:>|$)/i); - - // Generate HTML head and footer based on configuration and custom content - var htmlHead = (!configJSON.enableDirectoryListingWithDefaultHead || head == "" ? - (!headerHasHTMLTag ? - "Directory: " + decodeURIComponent(origHref).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") + "" : - customDirListingHeader.replace(//i, "Directory: " + decodeURIComponent(origHref).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") + "")) : - head.replace(//i, "Directory: " + decodeURIComponent(origHref).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") + "")) + - (!headerHasHTMLTag ? customDirListingHeader : "") + - "

Directory: " + decodeURIComponent(origHref).replace(/&/g, "&").replace(//g, ">") + "

" + (checkPathLevel(decodeURIComponent(origHref)) < 1 ? "" : ""); - - var htmlFoot = "
Filename Size Date
\"[RET]\"Return

" + (exposeServerVersion ? "SVR.JS/" + version + " (" + getOS() + "; " + (process.isBun ? ("Bun/v" + process.versions.bun + "; like Node.JS/" + process.version) : ("Node.JS/" + process.version)) + ")" : "SVR.JS").replace(/&/g, "&").replace(//g, ">") + (req.headers.host == undefined ? "" : " on " + String(req.headers.host).replace(/&/g, "&").replace(//g, ">")) + "

" + customDirListingFooter + (!configJSON.enableDirectoryListingWithDefaultHead || foot == "" ? "" : foot); - - if (fs.existsSync("." + decodeURIComponent(href) + "/.maindesc".replace(/\/+/g, "/"))) { - htmlFoot = "
" + fs.readFileSync("." + decodeURIComponent(href) + "/.maindesc".replace(/\/+/g, "/")) + htmlFoot; + try { + var hdhds = {}; + if (useBrotli && isCompressable) { + hdhds["Content-Encoding"] = "br"; + } else if (useDeflate && isCompressable) { + hdhds["Content-Encoding"] = "deflate"; + } else if (useGzip && isCompressable) { + hdhds["Content-Encoding"] = "gzip"; + } else { + if (ext == "html") { + hdhds["Content-Length"] = head.length + filelen + foot.length; + } else { + hdhds["Content-Length"] = filelen; + } } + if (ext != "html") hdhds["Accept-Ranges"] = "bytes"; + delete hdhds["Content-Type"]; + if (!(mime.contentType(ext) == false) && ext != "") hdhds["Content-Type"] = mime.contentType(ext); + if (fileETag) hdhds["ETag"] = fileETag; - fs.readdir("." + decodeURIComponent(href), function (err, list) { - try { - if (err) throw err; - list = list.sort(); - - // Function to get stats for all files in the directory - function getStatsForAllFilesI(fileList, callback, prefix, pushArray, index) { - if (fileList.length == 0) { - callback(pushArray); - return; - } - - fs.stat((prefix + "/" + fileList[index]).replace(/\/+/g, "/"), function (err, stats) { - if (err) { - fs.lstat((prefix + "/" + fileList[index]).replace(/\/+/g, "/"), function (err, stats) { - pushArray.push({ - name: fileList[index], - stats: err ? null : stats, - errored: true - }); - if (index < fileList.length - 1) { - getStatsForAllFilesI(fileList, callback, prefix, pushArray, index + 1); - } else { - callback(pushArray); - } - }); - } else { - pushArray.push({ - name: fileList[index], - stats: stats, - errored: false - }); - if (index < fileList.length - 1) { - getStatsForAllFilesI(fileList, callback, prefix, pushArray, index + 1); - } else { - callback(pushArray); - } - } - }); - } - - // Wrapper function to get stats for all files - function getStatsForAllFiles(fileList, prefix, callback) { - if (!prefix) prefix = ""; - getStatsForAllFilesI(fileList, callback, prefix, [], 0); - } - - // Get stats for all files in the directory and generate the listing - getStatsForAllFiles(list, "." + decodeURIComponent(href), function (filelist) { - var directoryListingRows = []; - for (var i = 0; i < filelist.length; i++) { - if (filelist[i].name[0] !== ".") { - var estats = filelist[i].stats; - var ename = filelist[i].name; - var eext = ename.match(/\.([^.]+)$/); - eext = eext ? eext[1] : ""; - var emime = eext ? mime.contentType(eext) : false; - if (filelist[i].errored) { - directoryListingRows.push( - "\"[BAD]\"" + - ename.replace(/&/g, "&").replace(//g, ">") + - "-" + - (estats ? estats.mtime.toDateString() : "-") + - "\r\n" - ); - } else { - var entry = "\"[alt]\"" + - ename.replace(/&/g, "&").replace(//g, ">") + - "" + - (estats.isDirectory() ? "-" : sizify(estats.size.toString())) + - "" + - estats.mtime.toDateString() + - "\r\n"; - - // Determine the file type and set the appropriate image and alt text - if (estats.isDirectory()) { - entry = entry.replace("[img]", "/.dirimages/directory.png").replace("[alt]", "[DIR]"); - } else if (!estats.isFile()) { - entry = "\"[alt]\"" + - ename.replace(/&/g, "&").replace(//g, ">") + - "-" + - estats.mtime.toDateString() + - "\r\n"; - - // Determine the special file types (block device, character device, etc.) - if (estats.isBlockDevice()) { - entry = entry.replace("[img]", "/.dirimages/hwdevice.png").replace("[alt]", "[BLK]"); - } else if (estats.isCharacterDevice()) { - entry = entry.replace("[img]", "/.dirimages/hwdevice.png").replace("[alt]", "[CHR]"); - } else if (estats.isFIFO()) { - entry = entry.replace("[img]", "/.dirimages/fifo.png").replace("[alt]", "[FIF]"); - } else if (estats.isSocket()) { - entry = entry.replace("[img]", "/.dirimages/socket.png").replace("[alt]", "[SCK]"); - } - } else if (ename.match(/README|LICEN[SC]E/i)) { - entry = entry.replace("[img]", "/.dirimages/important.png").replace("[alt]", "[IMP]"); - } else if (eext.match(/^(?:[xs]?html?|xml)$/i)) { - entry = entry.replace("[img]", "/.dirimages/html.png").replace("[alt]", (eext == "xml" ? "[XML]" : "[HTM]")); - } else if (eext == "js") { - entry = entry.replace("[img]", "/.dirimages/javascript.png").replace("[alt]", "[JS ]"); - } else if (eext == "php") { - entry = entry.replace("[img]", "/.dirimages/php.png").replace("[alt]", "[PHP]"); - } else if (eext == "css") { - entry = entry.replace("[img]", "/.dirimages/css.png").replace("[alt]", "[CSS]"); - } else if (emime && emime.split("/")[0] == "image") { - entry = entry.replace("[img]", "/.dirimages/image.png").replace("[alt]", (eext == "ico" ? "[ICO]" : "[IMG]")); - } else if (emime && emime.split("/")[0] == "font") { - entry = entry.replace("[img]", "/.dirimages/font.png").replace("[alt]", "[FON]"); - } else if (emime && emime.split("/")[0] == "audio") { - entry = entry.replace("[img]", "/.dirimages/audio.png").replace("[alt]", "[AUD]"); - } else if ((emime && emime.split("/")[0] == "text") || eext == "json") { - entry = entry.replace("[img]", "/.dirimages/text.png").replace("[alt]", (eext == "json" ? "[JSO]" : "[TXT]")); - } else if (emime && emime.split("/")[0] == "video") { - entry = entry.replace("[img]", "/.dirimages/video.png").replace("[alt]", "[VID]"); - } else if (eext.match(/^(?:zip|rar|bz2|[gb7x]z|lzma|tar)$/i)) { - entry = entry.replace("[img]", "/.dirimages/archive.png").replace("[alt]", "[ARC]"); - } else if (eext.match(/^(?:[id]mg|iso|flp)$/i)) { - entry = entry.replace("[img]", "/.dirimages/diskimage.png").replace("[alt]", "[DSK]"); - } else { - entry = entry.replace("[img]", "/.dirimages/other.png").replace("[alt]", "[OTH]"); - } - directoryListingRows.push(entry); - } - } - } - - // Push the information about empty directory - if (directoryListingRows.length == 0) { - directoryListingRows.push("No files found"); - } - - // Send the directory listing response - res.end(htmlHead + directoryListingRows.join("") + htmlFoot); - serverconsole.resmessage("Client successfully received content."); - }); - - } catch (err) { + if (req.method != "HEAD") { + var readStream = fs.createReadStream(readFrom); + readStream.on("error", function (err) { if (err.code == "ENOENT") { callServerError(404); serverconsole.errmessage("Resource not found."); @@ -4043,21 +3818,323 @@ if (!cluster.isPrimary) { } else { callServerError(500, err); } + }).on("open", function () { + try { + var resStream = {}; + if (useBrotli && isCompressable) { + resStream = zlib.createBrotliCompress(); + resStream.pipe(res); + } else if (useDeflate && isCompressable) { + resStream = zlib.createDeflateRaw(); + resStream.pipe(res); + } else if (useGzip && isCompressable) { + resStream = zlib.createGzip(); + resStream.pipe(res); + } else { + resStream = res; + } + if (ext == "html") { + function afterWriteCallback() { + readStream.on("end", function () { + resStream.end(foot); + }); + readStream.pipe(resStream, { + end: false + }); + } + res.writeHead(200, http.STATUS_CODES[200], hdhds); + if (!resStream.write(head)) { + resStream.on("drain", afterWriteCallback); + } else { + process.nextTick(afterWriteCallback); + } + } else { + res.writeHead(200, http.STATUS_CODES[200], hdhds); + readStream.pipe(resStream); + } + serverconsole.resmessage("Client successfully received content."); + } catch (err) { + callServerError(500, err); + } + }); + } else { + res.writeHead(200, http.STATUS_CODES[200], hdhds); + res.end(); + serverconsole.resmessage("Client successfully received content."); + } + } catch (err) { + callServerError(500, err); + } + } + } else if (stats.isDirectory()) { + // Check if directory listing is enabled in the configuration + if (checkForEnabledDirectoryListing(req.headers.host, req.socket ? req.socket.localAddress : undefined)) { + var customDirListingHeader = ""; + var customDirListingFooter = ""; + + function getCustomDirListingHeader(callback) { + fs.readFile(("." + dHref + "/.dirhead").replace(/\/+/g, "/"), function (err, data) { + if (err) { + if (err.code == "ENOENT" || err.code == "EISDIR") { + if (os.platform != "win32" || href != "/") { + fs.readFile(("." + dHref + "/HEAD.html").replace(/\/+/g, "/"), function (err, data) { + if (err) { + if (err.code == "ENOENT" || err.code == "EISDIR") { + callback(); + } else { + callServerError(500, err); + } + } else { + customDirListingHeader = data.toString(); + callback(); + } + }); + } else { + callback(); + } + } else { + callServerError(500, err); + } + } else { + customDirListingHeader = data.toString(); + callback(); } }); - } else { - // Directory listing is disabled, call 403 Forbidden error - callServerError(403); - serverconsole.errmessage("Directory listing is disabled."); } + + function getCustomDirListingFooter(callback) { + fs.readFile(("." + dHref + "/.dirfoot").replace(/\/+/g, "/"), function (err, data) { + if (err) { + if (err.code == "ENOENT" || err.code == "EISDIR") { + if (os.platform != "win32" || href != "/") { + fs.readFile(("." + dHref + "/FOOT.html").replace(/\/+/g, "/"), function (err, data) { + if (err) { + if (err.code == "ENOENT" || err.code == "EISDIR") { + callback(); + } else { + callServerError(500, err); + } + } else { + customDirListingFooter = data.toString(); + callback(); + } + }); + } else { + callback(); + } + } else { + callServerError(500, err); + } + } else { + customDirListingFooter = data.toString(); + callback(); + } + }); + } + + // Read custom header and footer content (if available) + getCustomDirListingHeader(function () { + getCustomDirListingFooter(function () { + // Check if custom header has HTML tag + var headerHasHTMLTag = customDirListingHeader.replace(/|$)/g, "").match(/])*(?:>|$)/i); + + // Generate HTML head and footer based on configuration and custom content + var htmlHead = (!configJSON.enableDirectoryListingWithDefaultHead || head == "" ? + (!headerHasHTMLTag ? + "Directory: " + decodeURIComponent(origHref).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") + "" : + customDirListingHeader.replace(//i, "Directory: " + decodeURIComponent(origHref).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") + "")) : + head.replace(//i, "Directory: " + decodeURIComponent(origHref).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") + "")) + + (!headerHasHTMLTag ? customDirListingHeader : "") + + "

Directory: " + decodeURIComponent(origHref).replace(/&/g, "&").replace(//g, ">") + "

" + (checkPathLevel(decodeURIComponent(origHref)) < 1 ? "" : ""); + + var htmlFoot = "
Filename Size Date
\"[RET]\"Return

" + (exposeServerVersion ? "SVR.JS/" + version + " (" + getOS() + "; " + (process.isBun ? ("Bun/v" + process.versions.bun + "; like Node.JS/" + process.version) : ("Node.JS/" + process.version)) + ")" : "SVR.JS").replace(/&/g, "&").replace(//g, ">") + (req.headers.host == undefined ? "" : " on " + String(req.headers.host).replace(/&/g, "&").replace(//g, ">")) + "

" + customDirListingFooter + (!configJSON.enableDirectoryListingWithDefaultHead || foot == "" ? "" : foot); + + if (fs.existsSync("." + decodeURIComponent(href) + "/.maindesc".replace(/\/+/g, "/"))) { + htmlFoot = "
" + fs.readFileSync("." + decodeURIComponent(href) + "/.maindesc".replace(/\/+/g, "/")) + htmlFoot; + } + + fs.readdir(readFrom, function (err, list) { + try { + if (err) throw err; + list = list.sort(); + + // Function to get stats for all files in the directory + function getStatsForAllFilesI(fileList, callback, prefix, pushArray, index) { + if (fileList.length == 0) { + callback(pushArray); + return; + } + + fs.stat((prefix + "/" + fileList[index]).replace(/\/+/g, "/"), function (err, stats) { + if (err) { + fs.lstat((prefix + "/" + fileList[index]).replace(/\/+/g, "/"), function (err, stats) { + pushArray.push({ + name: fileList[index], + stats: err ? null : stats, + errored: true + }); + if (index < fileList.length - 1) { + getStatsForAllFilesI(fileList, callback, prefix, pushArray, index + 1); + } else { + callback(pushArray); + } + }); + } else { + pushArray.push({ + name: fileList[index], + stats: stats, + errored: false + }); + if (index < fileList.length - 1) { + getStatsForAllFilesI(fileList, callback, prefix, pushArray, index + 1); + } else { + callback(pushArray); + } + } + }); + } + + // Wrapper function to get stats for all files + function getStatsForAllFiles(fileList, prefix, callback) { + if (!prefix) prefix = ""; + getStatsForAllFilesI(fileList, callback, prefix, [], 0); + } + + // Get stats for all files in the directory and generate the listing + getStatsForAllFiles(list, readFrom, function (filelist) { + var directoryListingRows = []; + for (var i = 0; i < filelist.length; i++) { + if (filelist[i].name[0] !== ".") { + var estats = filelist[i].stats; + var ename = filelist[i].name; + var eext = ename.match(/\.([^.]+)$/); + eext = eext ? eext[1] : ""; + var emime = eext ? mime.contentType(eext) : false; + if (filelist[i].errored) { + directoryListingRows.push( + "\"[BAD]\"" + + ename.replace(/&/g, "&").replace(//g, ">") + + "-" + + (estats ? estats.mtime.toDateString() : "-") + + "\r\n" + ); + } else { + var entry = "\"[alt]\"" + + ename.replace(/&/g, "&").replace(//g, ">") + + "" + + (estats.isDirectory() ? "-" : sizify(estats.size.toString())) + + "" + + estats.mtime.toDateString() + + "\r\n"; + + // Determine the file type and set the appropriate image and alt text + if (estats.isDirectory()) { + entry = entry.replace("[img]", "/.dirimages/directory.png").replace("[alt]", "[DIR]"); + } else if (!estats.isFile()) { + entry = "\"[alt]\"" + + ename.replace(/&/g, "&").replace(//g, ">") + + "-" + + estats.mtime.toDateString() + + "\r\n"; + + // Determine the special file types (block device, character device, etc.) + if (estats.isBlockDevice()) { + entry = entry.replace("[img]", "/.dirimages/hwdevice.png").replace("[alt]", "[BLK]"); + } else if (estats.isCharacterDevice()) { + entry = entry.replace("[img]", "/.dirimages/hwdevice.png").replace("[alt]", "[CHR]"); + } else if (estats.isFIFO()) { + entry = entry.replace("[img]", "/.dirimages/fifo.png").replace("[alt]", "[FIF]"); + } else if (estats.isSocket()) { + entry = entry.replace("[img]", "/.dirimages/socket.png").replace("[alt]", "[SCK]"); + } + } else if (ename.match(/README|LICEN[SC]E/i)) { + entry = entry.replace("[img]", "/.dirimages/important.png").replace("[alt]", "[IMP]"); + } else if (eext.match(/^(?:[xs]?html?|xml)$/i)) { + entry = entry.replace("[img]", "/.dirimages/html.png").replace("[alt]", (eext == "xml" ? "[XML]" : "[HTM]")); + } else if (eext == "js") { + entry = entry.replace("[img]", "/.dirimages/javascript.png").replace("[alt]", "[JS ]"); + } else if (eext == "php") { + entry = entry.replace("[img]", "/.dirimages/php.png").replace("[alt]", "[PHP]"); + } else if (eext == "css") { + entry = entry.replace("[img]", "/.dirimages/css.png").replace("[alt]", "[CSS]"); + } else if (emime && emime.split("/")[0] == "image") { + entry = entry.replace("[img]", "/.dirimages/image.png").replace("[alt]", (eext == "ico" ? "[ICO]" : "[IMG]")); + } else if (emime && emime.split("/")[0] == "font") { + entry = entry.replace("[img]", "/.dirimages/font.png").replace("[alt]", "[FON]"); + } else if (emime && emime.split("/")[0] == "audio") { + entry = entry.replace("[img]", "/.dirimages/audio.png").replace("[alt]", "[AUD]"); + } else if ((emime && emime.split("/")[0] == "text") || eext == "json") { + entry = entry.replace("[img]", "/.dirimages/text.png").replace("[alt]", (eext == "json" ? "[JSO]" : "[TXT]")); + } else if (emime && emime.split("/")[0] == "video") { + entry = entry.replace("[img]", "/.dirimages/video.png").replace("[alt]", "[VID]"); + } else if (eext.match(/^(?:zip|rar|bz2|[gb7x]z|lzma|tar)$/i)) { + entry = entry.replace("[img]", "/.dirimages/archive.png").replace("[alt]", "[ARC]"); + } else if (eext.match(/^(?:[id]mg|iso|flp)$/i)) { + entry = entry.replace("[img]", "/.dirimages/diskimage.png").replace("[alt]", "[DSK]"); + } else { + entry = entry.replace("[img]", "/.dirimages/other.png").replace("[alt]", "[OTH]"); + } + directoryListingRows.push(entry); + } + } + } + + // Push the information about empty directory + if (directoryListingRows.length == 0) { + directoryListingRows.push("No files found"); + } + + // Send the directory listing response + res.writeHead(200, http.STATUS_CODES[200], { + "Content-Type": "text/html; charset=utf-8" + }); + res.end(htmlHead + directoryListingRows.join("") + htmlFoot); + serverconsole.resmessage("Client successfully received content."); + }); + + } catch (err) { + if (err.code == "ENOENT") { + callServerError(404); + serverconsole.errmessage("Resource not found."); + } else if (err.code == "ENOTDIR") { + callServerError(404); // Assume that file doesn't exist. + serverconsole.errmessage("Resource not found."); + } else if (err.code == "EACCES") { + callServerError(403); + serverconsole.errmessage("Access denied."); + } else if (err.code == "ENAMETOOLONG") { + callServerError(414); + } else if (err.code == "EMFILE") { + callServerError(503); + } else if (err.code == "ELOOP") { + callServerError(508); // The symbolic link loop is detected during file system operations. + serverconsole.errmessage("Symbolic link loop detected."); + } else { + callServerError(500, err); + } + } + }); + }); + }); } else { - callServerError(501); - serverconsole.errmessage("SVR.JS doesn't support block devices, character devices, FIFOs nor sockets."); - return; + // Directory listing is disabled, call 403 Forbidden error + callServerError(403); + serverconsole.errmessage("Directory listing is disabled."); } + } else { + callServerError(501); + serverconsole.errmessage("SVR.JS doesn't support block devices, character devices, FIFOs nor sockets."); + return; } - }); - }; + } + }); } try { @@ -4118,11 +4195,19 @@ if (!cluster.isPrimary) { serverconsole.resmessage("URL sanitized: " + req.url + " => " + sanitizedURL); if (rewriteDirtyURLs) { req.url = sanitizedURL; - uobject = parseURL(req.url); + try { + uobject = parseURL(req.url, "http" + (req.socket.encrypted ? "s" : "") + "://" + (req.headers.host ? req.headers.host : (domain ? domain : "unknown.invalid"))); + } catch (err) { + // Return an 400 error + callServerError(400); + serverconsole.errmessage("Bad request!"); + return; + } search = uobject.search; href = uobject.pathname; - ext = path.extname(href).toLowerCase(); - ext = ext.substring(1, ext.length + 1); + ext = href.match(/[^\/]\.([^.]+)$/); + if(!ext) ext = ""; + else ext = ext[1].toLowerCase(); try { decodedHref = decodeURIComponent(href); } catch (err) { @@ -4267,7 +4352,7 @@ if (!cluster.isPrimary) { } } - var origHref = href; + origHref = href; // Add web root postfixes if (!isProxy) { @@ -4296,11 +4381,19 @@ if (!cluster.isPrimary) { if (urlWithPostfix != preparedReqUrl3) { serverconsole.resmessage("Added web root postfix: " + req.url + " => " + urlWithPostfix); req.url = urlWithPostfix; - uobject = parseURL(req.url); + try { + uobject = parseURL(req.url, "http" + (req.socket.encrypted ? "s" : "") + "://" + (req.headers.host ? req.headers.host : (domain ? domain : "unknown.invalid"))); + } catch (err) { + // Return an 400 error + callServerError(400); + serverconsole.errmessage("Bad request!"); + return; + } search = uobject.search; href = uobject.pathname; - ext = path.extname(href).toLowerCase(); - ext = ext.substring(1, ext.length + 1); + ext = href.match(/[^\/]\.([^.]+)$/); + if(!ext) ext = ""; + else ext = ext[1].toLowerCase(); try { decodedHref = decodeURIComponent(href); @@ -4331,11 +4424,19 @@ if (!cluster.isPrimary) { rewrittenAgainURL = url.format(rewrittenAgainURL); serverconsole.resmessage("URL sanitized: " + req.url + " => " + rewrittenAgainURL); req.url = rewrittenAgainURL; - uobject = parseURL(req.url); + try { + uobject = parseURL(req.url, "http" + (req.socket.encrypted ? "s" : "") + "://" + (req.headers.host ? req.headers.host : (domain ? domain : "unknown.invalid"))); + } catch (err) { + // Return an 400 error + callServerError(400); + serverconsole.errmessage("Bad request!"); + return; + } search = uobject.search; href = uobject.pathname; - ext = path.extname(href).toLowerCase(); - ext = ext.substring(1, ext.length + 1); + ext = href.match(/[^\/]\.([^.]+)$/); + if(!ext) ext = ""; + else ext = ext[1].toLowerCase(); try { decodedHref = decodeURIComponent(href); } catch (err) { @@ -4357,12 +4458,19 @@ if (!cluster.isPrimary) { if (rewrittenURL != req.url) { serverconsole.resmessage("URL rewritten: " + req.url + " => " + rewrittenURL); req.url = rewrittenURL; - uobject = parseURL(req.url); + try { + uobject = parseURL(req.url, "http" + (req.socket.encrypted ? "s" : "") + "://" + (req.headers.host ? req.headers.host : (domain ? domain : "unknown.invalid"))); + } catch (err) { + // Return an 400 error + callServerError(400); + serverconsole.errmessage("Bad request!"); + return; + } search = uobject.search; href = uobject.pathname; - ext = path.extname(href).toLowerCase(); - ext = ext.substring(1, ext.length + 1); - + ext = href.match(/[^\/]\.([^.]+)$/); + if(!ext) ext = ""; + else ext = ext[1].toLowerCase(); try { decodedHref = decodeURIComponent(href); } catch (err) { @@ -4392,11 +4500,19 @@ if (!cluster.isPrimary) { rewrittenAgainURL = url.format(rewrittenAgainURL); serverconsole.resmessage("URL sanitized: " + req.url + " => " + rewrittenAgainURL); req.url = rewrittenAgainURL; - uobject = parseURL(req.url); + try { + uobject = parseURL(req.url, "http" + (req.socket.encrypted ? "s" : "") + "://" + (req.headers.host ? req.headers.host : (domain ? domain : "unknown.invalid"))); + } catch (err) { + // Return an 400 error + callServerError(400); + serverconsole.errmessage("Bad request!"); + return; + } search = uobject.search; href = uobject.pathname; - ext = path.extname(href).toLowerCase(); - ext = ext.substring(1, ext.length + 1); + ext = href.match(/[^\/]\.([^.]+)$/); + if(!ext) ext = ""; + else ext = ext[1].toLowerCase(); try { decodedHref = decodeURIComponent(href); } catch (err) { @@ -4681,7 +4797,7 @@ if (!cluster.isPrimary) { serverconsole.reqmessage("Client is logged in as \"" + String(username).replace(/[\r\n]/g, "") + "\"."); authUser = username; redirectTrailingSlashes(function () { - modExecute(mods, vres(req, res, serverconsole, responseEnd, href, ext, uobject, search, "index.html", users, page404, head, foot, "", callServerError, getCustomHeaders, origHref, redirect, parsePostData, authUser)); + modExecute(mods, vres); }); } } catch (err) { @@ -4730,7 +4846,7 @@ if (!cluster.isPrimary) { } } else { redirectTrailingSlashes(function () { - modExecute(mods, vres(req, res, serverconsole, responseEnd, href, ext, uobject, search, "index.html", users, page404, head, foot, "", callServerError, getCustomHeaders, origHref, redirect, parsePostData, authUser)); + modExecute(mods, vres); }); } } diff --git a/tests.html b/tests.html index 9c46f72..67d7297 100644 --- a/tests.html +++ b/tests.html @@ -1,32 +1,96 @@ - SVR.JS 3.14.15 Tests + SVR.JS 3.15.0 Tests -

SVR.JS 3.14.15 Tests

+

SVR.JS 3.15.0 Tests

Directory (without trailing slash)

- +

Directory (with query)

- +

Directory (personalized)

- +

404 Error

- +

Server Side Javascript

- +

Proxy test

- +

URL rewriting test (/testdir_rewritten => /testdir)

- +