Compare commits

...

5 commits

12 changed files with 956 additions and 55 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
# Build output
/dist/
/out/
/core/
# Temporary files used by build script
/generatedAssets/

View file

@ -81,7 +81,7 @@ After installing the packages, build SVR.JS with this command:
```bash
npm run build
```
After running the command, you will get bundled SVR.JS script, around with built-in utilities and assets in the `dist` directory. You will also get a zip archive in `out` directory, that can be installed using SVR.JS installer
After running the command, you will get bundled SVR.JS script, around with built-in utilities and assets in the `dist` directory. You will also get a zip archive in `out` directory, that can be installed using SVR.JS installer. Additionally, you will get the SVR.JS Core package contents in the `core` directory, which you can publish by running `npm publish` in the `core` directory.
## Installation (built from source)
@ -118,6 +118,8 @@ You can read the [SVR.JS documentation](https://svrjs.org/docs) to get informati
The file structure for SVR.JS source code looks like this:
- .husky - Git hooks
- assets - files to copy into dist folder and to the archive
- core - contains SVR.JS Core
- coreAssets - files to copy into core folder
- dist - contains SVR.JS, assets, and SVR.JS utiltiies
- generatedAssets - assets generated by the build script
- out - contains SVR.JS zip archive

21
coreAssets/LICENSE Normal file
View file

@ -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.

64
coreAssets/README.md Normal file
View file

@ -0,0 +1,64 @@
# SVR.JS Core
SVR.JS Core is a library for static file serving, built from SVR.JS source code.
Example code (with Node.JS "http" module):
```javascript
var http = require("http");
var fs = require("fs");
var url = require("url");
var svrjsCore = require("svrjs-core").init(); // Initialize SVR.JS Core
var server = http.createServer(function (req,res) {
if(url.parse(req.url).pathname == "/useragent") {
res.writeHead(200, "OK", {"content-type": "text-plain"}); // Output as plain text
res.end("Your user agent: " + req.headers["user-agent"]); // Send user agent
} else {
svrjsCore(req,res); // Serve static content
}
}).listen(8888);
```
Example code (with Express):
```javascript
var express = require("express");
var svrjsCore = require("svrjs-core");
var app = express();
app.use(svrjsCore());
app.listen(3000);
```
## Methods
### *svrjsCore([config])*
Parameters:
- *config* - the SVR.JS Core configuration (optional, *Object*)
Returns: the request handler for use in Node.JS HTTP server or Express, with three parameters (*req*, *res*, and optional *next*)
The *config* object is almost the same format as SVR.JS configuration. You can read about SVR.JS configuration properties in [the SVR.JS documentation](https://svrjs.org/docs/config/configuration).
However, only these SVR.JS configuration properties apply to SVR.JS Core:
- *users*
- *page404*
- *enableCompression*
- *customHeaders*
- *enableDirectoryListing*
- *enableDirectoryListingWithDefaultHead*
- *serverAdministratorEmail*
- *stackHidden*
- *exposeServerVersion*
- *dontCompress*
- *enableIPSpoofing*
- *enableETag*
- *rewriteDirtyURLs*
- *errorPages*
- *disableTrailingSlashRedirects*
- *allowDoubleSlashes*
### *svrjsCore.init([config])*
An alias to the *svrjsCore()* function

View file

@ -7,6 +7,11 @@ 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 corePackageJSON = svrjsCoreInfo.packageJSON;
const isDev = process.env.NODE_ENV == "development";
// Create the dist directory if it doesn't exist
@ -19,7 +24,12 @@ if (!fs.existsSync(__dirname + "/dist/temp"))
fs.mkdirSync(__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");
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
@ -171,23 +181,25 @@ if (!isDev) {
__dirname + "/package.json",
__dirname + "/svrjs.json"
]);
watcher.on("change", () => {
try {
generateAssets();
} catch (err) {
console.error("There is a problem when regenerating assets!");
console.error("Stack:");
console.error(err.stack);
}
}).on("ready", () => {
try {
generateAssets();
} catch (err) {
console.error("There is a problem when regenerating assets!");
console.error("Stack:");
console.error(err.stack);
}
});
watcher
.on("change", () => {
try {
generateAssets();
} catch (err) {
console.error("There is a problem when regenerating assets!");
console.error("Stack:");
console.error(err.stack);
}
})
.on("ready", () => {
try {
generateAssets();
} catch (err) {
console.error("There is a problem when regenerating assets!");
console.error("Stack:");
console.error(err.stack);
}
});
}
if (!isDev) {
@ -243,39 +255,93 @@ if (!isDev) {
target: "es2017"
})
.then(() => {
const archiveName =
"svr.js." +
version.toLowerCase().replace(/[^0-9a-z]+/g, ".") +
".zip";
const output = fs.createWriteStream(
__dirname + "/out/" + archiveName
const dependencies =
JSON.parse(fs.readFileSync(__dirname + "/package.json"))
.dependencies || {};
const coreDependencyNames = Object.keys(dependencies).filter(
(dependency) => externalPackages.indexOf(dependency) != -1
);
const archive = archiver("zip", {
zlib: { level: 9 }
});
archive.pipe(output);
const packageJSON = Object.assign({}, corePackageJSON);
// Add everything in the "dist" directory except for "svr.js" and "svr.compressed"
archive.glob("**/*", {
cwd: __dirname + "/dist",
ignore: ["svr.js", "svr.compressed"],
dot: true
});
// Create a stream for the "svr.compressed" file
const compressedSVRJSFileStream = fs
.createReadStream(__dirname + "/dist/svr.js")
.pipe(
zlib.createGzip({
level: 9
})
);
archive.append(compressedSVRJSFileStream, { name: "svr.compressed" });
archive.append(
'const zlib = require("zlib");\nconst fs = require("fs");\nconsole.log("Deleting SVR.JS stub...");\nfs.unlinkSync("svr.js");\nconsole.log("Decompressing SVR.JS...");\nconst script = zlib.gunzipSync(fs.readFileSync("svr.compressed"));\nfs.unlinkSync("svr.compressed");\nfs.writeFileSync("svr.js",script);\nconsole.log("Restart SVR.JS to get server interface.");',
{ name: "svr.js" }
// Add package.json properties
packageJSON.version = version;
packageJSON.main = "./svr.core.js";
packageJSON.dependencies = coreDependencyNames.reduce(
(previousDependencies, dependency) => {
previousDependencies[dependency] = dependencies[dependency];
return previousDependencies;
},
{}
);
archive.finalize();
// 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, ".") +
".zip";
const output = fs.createWriteStream(
__dirname + "/out/" + archiveName
);
const archive = archiver("zip", {
zlib: { level: 9 }
});
archive.pipe(output);
// Add everything in the "dist" directory except for "svr.js" and "svr.compressed"
archive.glob("**/*", {
cwd: __dirname + "/dist",
ignore: ["svr.js", "svr.compressed"],
dot: true
});
// Create a stream for the "svr.compressed" file
const compressedSVRJSFileStream = fs
.createReadStream(__dirname + "/dist/svr.js")
.pipe(
zlib.createGzip({
level: 9
})
);
archive.append(compressedSVRJSFileStream, {
name: "svr.compressed"
});
archive.append(
'const zlib = require("zlib");\nconst fs = require("fs");\nconsole.log("Deleting SVR.JS stub...");\nfs.unlinkSync("svr.js");\nconsole.log("Decompressing SVR.JS...");\nconst script = zlib.gunzipSync(fs.readFileSync("svr.compressed"));\nfs.unlinkSync("svr.compressed");\nfs.writeFileSync("svr.js",script);\nconsole.log("Restart SVR.JS to get server interface.");',
{ name: "svr.js" }
);
archive.finalize();
})
.catch((err) => {
throw err;
});
})
.catch((err) => {
throw err;

View file

@ -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",

672
src/core.js Normal file
View file

@ -0,0 +1,672 @@
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/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, "&lt;")
.replace(/>/g, "&gt;")
)
.replace(/{errorDesc}/g, serverHTTPErrorDescs[errorCode])
.replace(
/{stack}/g,
stack
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\r\n/g, "<br/>")
.replace(/\n/g, "<br/>")
.replace(/\r/g, "<br/>")
.replace(/ {2}/g, "&nbsp;&nbsp;")
)
.replace(
/{path}/g,
req.url
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
)
.replace(
/{server}/g,
"" +
(
config.generateServerString() +
(!config.exposeModsInErrorPages || extName == undefined
? ""
: " " + extName)
)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;") +
(req.headers.host == undefined || req.isProxy
? ""
: " on " +
String(req.headers.host)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;"))
)
.replace(
/{contact}/g,
config.serverAdministratorEmail
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.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(
`<!DOCTYPE html><html><head><title>{errorMessage}</title><meta name="viewport" content="width=device-width, initial-scale=1.0" /><style>${defaultPageCSS}</style></head><body><h1>{errorMessage}</h1><p>{errorDesc}</p>${
additionalError == 404
? ""
: "<p>Additionally, a {additionalError} error occurred while loading an error page.</p>"
}<p><i>{server}</i></p></body></html>`
.replace(
/{errorMessage}/g,
errorCode.toString() +
" " +
statusCodes[errorCode]
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
)
.replace(/{errorDesc}/g, serverHTTPErrorDescs[errorCode])
.replace(
/{stack}/g,
stack
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\r\n/g, "<br/>")
.replace(/\n/g, "<br/>")
.replace(/\r/g, "<br/>")
.replace(/ {2}/g, "&nbsp;&nbsp;")
)
.replace(
/{path}/g,
req.url
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
)
.replace(
/{server}/g,
"" +
(
config.generateServerString() +
(!config.exposeModsInErrorPages || extName == undefined
? ""
: " " + extName)
)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;") +
(req.headers.host == undefined || req.isProxy
? ""
: " on " +
String(req.headers.host)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;"))
)
.replace(
/{contact}/g,
config.serverAdministratorEmail
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\./g, "[dot]")
.replace(/@/g, "[at]")
)
.replace(/{additionalError}/g, additionalError.toString())
); // Replace placeholders in error response
res.end();
}
});
}
});
};
// Server error calling method
res.error = (errorCode, extName, stack, ch) => {
if (typeof errorCode !== "number") {
throw new TypeError("HTTP error code parameter needs to be an integer.");
}
// Handle optional parameters
if (extName && typeof extName === "object") {
ch = stack;
stack = extName;
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]"
) {
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."
);
}
if (next) {
// Invoke next() handler, like when it is used in Express
if (errorCode == 500) {
next(new Error("Internal SVR.JS core error"));
} else {
next();
}
} else {
// Invoke default server error handler
defaultServerError(errorCode, extName, stack, ch);
}
};
// Function to perform HTTP redirection to a specified destination URL
res.redirect = (destination, isTemporary, keepMethod, customHeaders) => {
// If keepMethod is a object, then save it to customHeaders
if (typeof keepMethod == "object") customHeaders = keepMethod;
// If isTemporary is a object, then save it to customHeaders
if (typeof isTemporary == "object") customHeaders = isTemporary;
// If customHeaders are not provided, get the default custom headers
if (customHeaders === undefined) customHeaders = config.getCustomHeaders();
// Set the "Location" header to the destination URL
customHeaders["Location"] = destination;
// Determine the status code for redirection based on the isTemporary and keepMethod flags
const statusCode = keepMethod
? isTemporary
? 307
: 308
: isTemporary
? 302
: 301;
// Write the response header with the appropriate status code and message
res.writeHead(statusCode, statusCodes[statusCode], customHeaders);
// End the response
res.end();
// Return from the function
return;
};
try {
res.head = fs.existsSync(`${config.wwwroot}/.head`)
? fs.readFileSync(`${config.wwwroot}/.head`).toString()
: fs.existsSync(`${config.wwwroot}/head.html`)
? fs.readFileSync(`${config.wwwroot}/head.html`).toString()
: ""; // header
res.foot = fs.existsSync(`${config.wwwroot}/.foot`)
? fs.readFileSync(`${config.wwwroot}/.foot`).toString()
: fs.existsSync(`${config.wwwroot}/foot.html`)
? fs.readFileSync(`${config.wwwroot}/foot.html`).toString()
: ""; // footer
} catch (err) {
res.error(500, err);
}
// Authenticated user variable
req.authUser = null;
if (req.url == "*") {
// Handle "*" URL
if (req.method == "OPTIONS") {
// Respond with list of methods
let hdss = config.getCustomHeaders();
hdss["Allow"] = "GET, POST, HEAD, OPTIONS";
res.writeHead(204, statusCodes[204], hdss);
res.end();
return;
} else {
// SVR.JS doesn't understand that request, so throw an 400 error
res.error(400);
return;
}
}
if (req.headers["expect"] && req.headers["expect"] != "100-continue") {
// Expectations not met.
res.error(417);
return;
}
if (req.method == "CONNECT") {
// CONNECT requests should be handled in "connect" event.
res.error(501);
return;
}
if (!isForwardedValid) {
res.error(400);
return;
}
try {
req.parsedURL = parseURL(
req.url,
"http" +
(req.socket.encrypted ? "s" : "") +
"://" +
(req.headers.host
? req.headers.host
: config.domain
? config.domain
: "unknown.invalid")
);
// req.originalParsedURL fallback
req.originalParsedURL = req.parsedURL;
} catch (err) {
res.error(400, err);
return;
}
let index = 0;
// Call the next middleware function
const nextMiddleware = () => {
let currentMiddleware = middleware[index++];
while (
req.isProxy &&
currentMiddleware &&
currentMiddleware.proxySafe !== false &&
!(currentMiddleware.proxySafe || currentMiddleware.proxy)
) {
currentMiddleware = middleware[index++];
}
if (currentMiddleware) {
try {
currentMiddleware(req, res, logFacilities, config, nextMiddleware);
} catch (err) {
res.error(500, err);
}
} else {
res.error(404);
}
};
// Handle middleware
nextMiddleware();
}
function init(config) {
if (config) coreConfig = config;
if (coreConfig.users === undefined) coreConfig.users = [];
if (coreConfig.page404 === undefined) coreConfig.page404 = "404.html";
if (coreConfig.enableCompression === undefined)
coreConfig.enableCompression = true;
if (coreConfig.customHeaders === undefined) coreConfig.customHeaders = {};
if (coreConfig.enableDirectoryListing === undefined)
coreConfig.enableDirectoryListing = true;
if (coreConfig.enableDirectoryListingWithDefaultHead === undefined)
coreConfig.enableDirectoryListingWithDefaultHead = false;
if (coreConfig.serverAdministratorEmail === undefined)
coreConfig.serverAdministratorEmail = "[no contact information]";
if (coreConfig.stackHidden === undefined) coreConfig.stackHidden = false;
if (coreConfig.exposeServerVersion === undefined)
coreConfig.exposeServerVersion = true;
if (coreConfig.dontCompress === undefined)
coreConfig.dontCompress = [
"/.*\\.ipxe$/",
"/.*\\.(?:jpe?g|png|bmp|tiff|jfif|gif|webp)$/",
"/.*\\.(?:[id]mg|iso|flp)$/",
"/.*\\.(?:zip|rar|bz2|[gb7x]z|lzma|tar)$/",
"/.*\\.(?:mp[34]|mov|wm[av]|avi|webm|og[gv]|mk[va])$/"
];
if (coreConfig.enableIPSpoofing === undefined)
coreConfig.enableIPSpoofing = false;
if (coreConfig.enableETag === undefined) coreConfig.enableETag = true;
if (coreConfig.rewriteDirtyURLs === undefined)
coreConfig.rewriteDirtyURLs = false;
if (coreConfig.errorPages === undefined) coreConfig.errorPages = [];
if (coreConfig.disableTrailingSlashRedirects === undefined)
coreConfig.disableTrailingSlashRedirects = false;
if (coreConfig.allowDoubleSlashes === undefined)
coreConfig.allowDoubleSlashes = false;
// You wouldn't use SVR.JS mods in SVR.JS Core
coreConfig.exposeModsInErrorPages = false;
return requestHandler;
}
module.exports = init;
module.exports.init = init;

View file

@ -706,6 +706,9 @@ function requestHandler(req, res) {
? config.domain
: "unknown.invalid")
);
// req.originalParsedURL fallback
req.originalParsedURL = req.parsedURL;
} catch (err) {
res.error(400, err);
return;
@ -714,7 +717,7 @@ function requestHandler(req, res) {
let index = 0;
// Call the next middleware function
const next = () => {
const nextMiddleware = () => {
let currentMiddleware = middleware[index++];
while (
req.isProxy &&
@ -726,7 +729,7 @@ function requestHandler(req, res) {
}
if (currentMiddleware) {
try {
currentMiddleware(req, res, logFacilities, config, next);
currentMiddleware(req, res, logFacilities, config, nextMiddleware);
} catch (err) {
res.error(500, err);
}
@ -736,7 +739,7 @@ function requestHandler(req, res) {
};
// Handle middleware
next();
nextMiddleware();
}
module.exports = (serverconsoleO, middlewareO) => {

View file

@ -854,7 +854,7 @@ if (!disableMods) {
}
// Middleware
let middleware = [
const middleware = [
require("./middleware/urlSanitizer.js"),
require("./middleware/redirects.js"),
require("./middleware/blocklist.js"),

View file

@ -980,7 +980,32 @@ module.exports = (req, res, logFacilities, config, next) => {
} else if (dirImagesMissing) {
fs.stat(readFrom, (e, s) => {
if (e || !s.isFile()) {
properDirectoryListingAndStaticFileServe();
if (err.code == "ENOENT") {
res.error(404);
logFacilities.errmessage("Resource not found.");
return;
} else if (err.code == "ENOTDIR") {
res.error(404); // Assume that file doesn't exist.
logFacilities.errmessage("Resource not found.");
return;
} else if (err.code == "EACCES") {
res.error(403);
logFacilities.errmessage("Access denied.");
return;
} else if (err.code == "ENAMETOOLONG") {
res.error(414);
return;
} else if (err.code == "EMFILE") {
res.error(503);
return;
} else if (err.code == "ELOOP") {
res.error(508); // The symbolic link loop is detected during file system operations.
logFacilities.errmessage("Symbolic link loop detected.");
return;
} else {
res.error(500, err);
return;
}
} else {
stats = s;
properDirectoryListingAndStaticFileServe();

View file

@ -0,0 +1,22 @@
const svrjsCoreInfo = require("../../svrjs.core.json");
const { name } = svrjsCoreInfo;
const svrjsInfo = require("../../svrjs.json");
const { version } = svrjsInfo;
const getOS = require("./getOS.js");
function generateServerString(exposeServerVersion) {
return exposeServerVersion
? `${name.replace(/ /g, "-")}/${version} (${getOS()}; ${
process.isBun
? "Bun/v" + process.versions.bun + "; like Node.JS/" + process.version
: process.versions && process.versions.deno
? "Deno/v" +
process.versions.deno +
"; like Node.JS/" +
process.version
: "Node.JS/" + process.version
})`
: name.replace(/ /g, "-");
}
module.exports = generateServerString;

25
svrjs.core.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "SVR.JS Core",
"externalPackages": ["mime-types"],
"packageJSON": {
"name": "svrjs-core",
"description": "A library for static file serving, built from SVR.JS source code.",
"repository": {
"type": "git",
"url": "https://git.svrjs.org/svrjs/svrjs.git"
},
"keywords": [
"static",
"web",
"server",
"files",
"mime",
"middleware"
],
"homepage": "https://svrjs.org",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
}
}