diff --git a/.gitignore b/.gitignore
index f860352..117aff9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
# Build output
/dist/
/out/
+/core/
# Temporary files used by build script
/generatedAssets/
diff --git a/coreAssets/LICENSE b/coreAssets/LICENSE
new file mode 100644
index 0000000..f382250
--- /dev/null
+++ b/coreAssets/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018-2024 SVR.JS
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/coreAssets/README.md b/coreAssets/README.md
new file mode 100644
index 0000000..cf1ff04
--- /dev/null
+++ b/coreAssets/README.md
@@ -0,0 +1,3 @@
+# SVR.JS Core
+
+TODO
\ No newline at end of file
diff --git a/esbuild.config.js b/esbuild.config.js
index 5782c40..3380da2 100644
--- a/esbuild.config.js
+++ b/esbuild.config.js
@@ -7,6 +7,10 @@ const archiver = require("archiver");
const chokidar = require("chokidar");
const svrjsInfo = JSON.parse(fs.readFileSync(__dirname + "/svrjs.json"));
const { version } = svrjsInfo;
+const svrjsCoreInfo = JSON.parse(fs.readFileSync(__dirname + "/svrjs.core.json"));
+const { externalPackages } = svrjsCoreInfo;
+const coreVersion = svrjsCoreInfo.version;
+const corePackageJSON = svrjsCoreInfo.packageJSON;
const isDev = process.env.NODE_ENV == "development";
// Create the dist directory if it doesn't exist
@@ -21,6 +25,9 @@ if (!fs.existsSync(__dirname + "/dist/temp"))
// Create the out directory if it doesn't exist and if not building for development
if (!isDev && !fs.existsSync(__dirname + "/out")) fs.mkdirSync(__dirname + "/out");
+// Create the core directory if it doesn't exist and if not building for development
+if (!isDev && !fs.existsSync(__dirname + "/core")) fs.mkdirSync(__dirname + "/core");
+
function generateAssets() {
// Variables from "svrjs.json" file
const svrjsInfo = JSON.parse(fs.readFileSync(__dirname + "/svrjs.json"));
@@ -243,6 +250,44 @@ if (!isDev) {
target: "es2017"
})
.then(() => {
+ const dependencies = JSON.parse(fs.readFileSync(__dirname + "/package.json")).dependencies || {};
+ const coreDependencyNames = Object.keys(dependencies).filter((dependency) => externalPackages.indexOf(dependency) != -1);
+ const packageJSON = Object.assign({}, corePackageJSON);
+
+ // Add package.json properties
+ packageJSON.version = coreVersion;
+ packageJSON.main = "./svr.core.js";
+ packageJSON.dependencies = coreDependencyNames.reduce((previousDependencies, dependency) => {
+ previousDependencies[dependency] = dependencies[dependency];
+ return previousDependencies;
+ }, {});
+
+ // Write package.json
+ fs.writeFileSync(__dirname + "/core/package.json", JSON.stringify(packageJSON, null, 2));
+
+ // Build SVR.JS Core
+ esbuild
+ .build({
+ entryPoints: ["src/core.js"],
+ bundle: true,
+ outfile: "core/svr.core.js",
+ platform: "node",
+ target: "es2017",
+ external: coreDependencyNames,
+ plugins: [
+ esbuildCopyPlugin.copy({
+ resolveFrom: __dirname,
+ assets: {
+ from: ["./coreAssets/**/*"],
+ to: ["./core"]
+ },
+ globbyOptions: {
+ dot: true
+ }
+ })
+ ]
+ })
+ .then(() => {
const archiveName =
"svr.js." +
version.toLowerCase().replace(/[^0-9a-z]+/g, ".") +
@@ -280,6 +325,10 @@ if (!isDev) {
.catch((err) => {
throw err;
});
+ })
+ .catch((err) => {
+ throw err;
+ });
})
.catch((err) => {
throw err;
diff --git a/package.json b/package.json
index 6a18ce4..b51a6fb 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"scripts": {
"build": "npm run clean && NODE_ENV=production node esbuild.config.js",
"cz": "cz",
- "clean": "rimraf dist && rimraf out && rimraf generatedAssets",
+ "clean": "rimraf dist && rimraf out && rimraf generatedAssets && rimraf core",
"dev": "npm run clean && concurrently \"NODE_ENV=development node esbuild.config.js\" \"wait-on dist/svr.js && nodemon dist/svr.js --stdout-notty --no-save-config\"",
"lint": "eslint --no-error-on-unmatched-pattern src/**/*.js src/*.js tests/**/*.test.js tests/**/*.js tests/*.test.js tests/*.js",
"lint:fix": "npm run lint -- --fix",
diff --git a/src/core.js b/src/core.js
new file mode 100644
index 0000000..d4dad72
--- /dev/null
+++ b/src/core.js
@@ -0,0 +1,633 @@
+const fs = require("fs");
+const net = require("net");
+const defaultPageCSS = require("./res/defaultPageCSS.js");
+const generateErrorStack = require("./utils/generateErrorStack.js");
+const serverHTTPErrorDescs = require("./res/httpErrorDescriptions.js");
+const fixNodeMojibakeURL = require("./utils/urlMojibakeFixer.js");
+const ipMatch = require("./utils/ipMatch.js");
+const matchHostname = require("./utils/matchHostname.js");
+const generateServerStringCore = require("./utils/generateServerStringCore.js");
+const parseURL = require("./utils/urlParser.js");
+const deepClone = require("./utils/deepClone.js");
+const statusCodes = require("./res/statusCodes.js");
+
+const middleware = [
+ require("./middleware/urlSanitizer.js"),
+ require("./middleware/rewriteURL.js"),
+ require("./middleware/redirectTrailingSlashes.js"),
+ require("./middleware/defaultHandlerChecks.js"),
+ require("./middleware/staticFileServingAndDirectoryListings.js")
+];
+let coreConfig = {};
+
+function requestHandler(req, res, next) {
+ // SVR.JS log facilities (stubs in SVR.JS core)
+ const logFacilities = {
+ climessage: () => {},
+ reqmessage: () => {},
+ resmessage: () => {},
+ errmessage: () => {},
+ locerrmessage: () => {},
+ locwarnmessage: () => {},
+ locmessage: () => {}
+ };
+
+ // SVR.JS configuration object (modified)
+ const config = deepClone(coreConfig);
+
+ config.generateServerString = () =>
+ generateServerStringCore(config.exposeServerVersion);
+
+ // Determine the webroot from the current working directory if it is not configured
+ if (config.wwwroot === undefined) config.wwwroot = process.cwd();
+
+ // getCustomHeaders() in SVR.JS 3.x
+ config.getCustomHeaders = () => {
+ let ph = Object.assign({}, config.customHeaders);
+ if (config.customHeadersVHost) {
+ let vhostP = null;
+ config.customHeadersVHost.every((vhost) => {
+ if (
+ matchHostname(vhost.host, req.headers.host) &&
+ ipMatch(vhost.ip, req.socket ? req.socket.localAddress : undefined)
+ ) {
+ vhostP = vhost;
+ return false;
+ } else {
+ return true;
+ }
+ });
+ if (vhostP && vhostP.headers) ph = { ...ph, ...vhostP.headers };
+ }
+ Object.keys(ph).forEach((phk) => {
+ if (typeof ph[phk] == "string")
+ ph[phk] = ph[phk].replace(/\{path\}/g, req.url);
+ });
+ 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 = (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((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 = (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);
+ if (req.socket && req.socket.server)
+ req.socket.server.emit("clientError", err, req.socket);
+ }
+ }
+
+ req.url = fixNodeMojibakeURL(req.url);
+
+ req.isProxy = false;
+
+ if (req.socket == 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;
+ // eslint-disable-next-line no-unused-vars
+ } catch (err) {
+ // Address setting failed
+ }
+ } else {
+ isForwardedValid = false;
+ }
+ }
+ }
+
+ // Process the Host header
+ 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(/\.$/, "");
+ }
+
+ // Header and footer placeholders
+ res.head = "";
+ res.foot = "";
+
+ res.responseEnd = (body) => {
+ // If body is Buffer, then it is converted to String anyway.
+ res.write(res.head + body + res.foot);
+ res.end();
+ };
+
+ const defaultServerError = (errorCode, extName, stack, ch) => {
+ // Determine error file
+ const getErrorFileName = (list, callback, _i) => {
+ const medCallback = (p) => {
+ if (p) callback(p);
+ else {
+ if (errorCode == 404) {
+ fs.access(config.page404, fs.constants.F_OK, (err) => {
+ if (err) {
+ fs.access(
+ config.wwwroot + "/." + errorCode.toString(),
+ fs.constants.F_OK,
+ (err) => {
+ try {
+ if (err) {
+ callback(errorCode.toString() + ".html");
+ } else {
+ callback(config.wwwroot + "/." + errorCode.toString());
+ }
+ } catch (err2) {
+ res.error(500, err2);
+ }
+ }
+ );
+ } else {
+ try {
+ callback(config.page404);
+ } catch (err2) {
+ res.error(500, err2);
+ }
+ }
+ });
+ } else {
+ fs.access(
+ config.wwwroot + "/." + errorCode.toString(),
+ fs.constants.F_OK,
+ (err) => {
+ try {
+ if (err) {
+ callback(errorCode.toString() + ".html");
+ } else {
+ callback(config.wwwroot + "/." + errorCode.toString());
+ }
+ } catch (err2) {
+ res.error(500, err2);
+ }
+ }
+ );
+ }
+ }
+ };
+
+ if (!_i) _i = 0;
+ if (_i >= list.length) {
+ medCallback(false);
+ return;
+ }
+
+ if (
+ list[_i].scode != errorCode ||
+ !(
+ matchHostname(list[_i].host, req.headers.host) &&
+ ipMatch(list[_i].ip, req.socket ? req.socket.localAddress : undefined)
+ )
+ ) {
+ getErrorFileName(list, callback, _i + 1);
+ return;
+ } else {
+ fs.access(list[_i].path, fs.constants.F_OK, (err) => {
+ if (err) {
+ getErrorFileName(list, callback, _i + 1);
+ } else {
+ medCallback(list[_i].path);
+ }
+ });
+ }
+ };
+
+ getErrorFileName(config.errorPages, (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"));
+
+ // Hide the error stack if specified
+ if (config.stackHidden) stack = "[error stack hidden]";
+
+ // Validate the error code and handle unknown codes
+ if (serverHTTPErrorDescs[errorCode] === undefined) {
+ res.error(501, extName, stack);
+ } else {
+ // Process custom headers if provided
+ let cheaders = { ...config.getCustomHeaders(), ...ch };
+
+ cheaders["Content-Type"] = "text/html; charset=utf-8";
+
+ // Set default Allow header for 405 error if not provided
+ if (errorCode == 405 && !cheaders["Allow"])
+ cheaders["Allow"] = "GET, POST, HEAD, OPTIONS";
+
+ // Read the error file and replace placeholders with error information
+ fs.readFile(errorFile, (err, data) => {
+ try {
+ if (err) throw err;
+ res.writeHead(errorCode, statusCodes[errorCode], cheaders);
+ res.responseEnd(
+ data
+ .toString()
+ .replace(
+ /{errorMessage}/g,
+ errorCode.toString() +
+ " " +
+ statusCodes[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.generateServerString() +
+ (!config.exposeModsInErrorPages || extName == undefined
+ ? ""
+ : " " + extName)
+ )
+ .replace(/&/g, "&")
+ .replace(//g, ">") +
+ (req.headers.host == undefined || req.isProxy
+ ? ""
+ : " on " +
+ String(req.headers.host)
+ .replace(/&/g, "&")
+ .replace(//g, ">"))
+ )
+ .replace(
+ /{contact}/g,
+ config.serverAdministratorEmail
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/\./g, "[dot]")
+ .replace(/@/g, "[at]")
+ )
+ ); // Replace placeholders in error response
+ } catch (err) {
+ let additionalError = 500;
+ // Handle additional error cases
+ if (err.code == "ENOENT") {
+ additionalError = 404;
+ } else if (err.code == "ENOTDIR") {
+ additionalError = 404; // Assume that file doesn't exist
+ } else if (err.code == "EACCES") {
+ additionalError = 403;
+ } else if (err.code == "ENAMETOOLONG") {
+ additionalError = 414;
+ } else if (err.code == "EMFILE") {
+ additionalError = 503;
+ } else if (err.code == "ELOOP") {
+ additionalError = 508;
+ }
+
+ res.writeHead(errorCode, statusCodes[errorCode], cheaders);
+ res.write(
+ `
{errorDesc}
${ + additionalError == 404 + ? "" + : "Additionally, a {additionalError} error occurred while loading an error page.
" + }{server}
` + .replace( + /{errorMessage}/g, + errorCode.toString() + + " " + + statusCodes[errorCode] + .replace(/&/g, "&") + .replace(//g, ">") + ) + .replace(/{errorDesc}/g, serverHTTPErrorDescs[errorCode]) + .replace( + /{stack}/g, + stack + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\r\n/g, "