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(("
{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, "{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, "