Compare commits
5 commits
1174a348b6
...
79b506a4f0
Author | SHA1 | Date | |
---|---|---|---|
79b506a4f0 | |||
e0536620d6 | |||
50465e4a6c | |||
c9bbd0e202 | |||
d34d4bcc2d |
12 changed files with 956 additions and 55 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
# Build output
|
||||
/dist/
|
||||
/out/
|
||||
/core/
|
||||
|
||||
# Temporary files used by build script
|
||||
/generatedAssets/
|
||||
|
|
|
@ -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
21
coreAssets/LICENSE
Normal 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
64
coreAssets/README.md
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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
672
src/core.js
Normal 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, "<")
|
||||
.replace(/>/g, ">")
|
||||
)
|
||||
.replace(/{errorDesc}/g, serverHTTPErrorDescs[errorCode])
|
||||
.replace(
|
||||
/{stack}/g,
|
||||
stack
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\r\n/g, "<br/>")
|
||||
.replace(/\n/g, "<br/>")
|
||||
.replace(/\r/g, "<br/>")
|
||||
.replace(/ {2}/g, " ")
|
||||
)
|
||||
.replace(
|
||||
/{path}/g,
|
||||
req.url
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
)
|
||||
.replace(
|
||||
/{server}/g,
|
||||
"" +
|
||||
(
|
||||
config.generateServerString() +
|
||||
(!config.exposeModsInErrorPages || extName == undefined
|
||||
? ""
|
||||
: " " + extName)
|
||||
)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">") +
|
||||
(req.headers.host == undefined || req.isProxy
|
||||
? ""
|
||||
: " on " +
|
||||
String(req.headers.host)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">"))
|
||||
)
|
||||
.replace(
|
||||
/{contact}/g,
|
||||
config.serverAdministratorEmail
|
||||
.replace(/&/g, "&")
|
||||
.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(
|
||||
`<!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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
)
|
||||
.replace(/{errorDesc}/g, serverHTTPErrorDescs[errorCode])
|
||||
.replace(
|
||||
/{stack}/g,
|
||||
stack
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\r\n/g, "<br/>")
|
||||
.replace(/\n/g, "<br/>")
|
||||
.replace(/\r/g, "<br/>")
|
||||
.replace(/ {2}/g, " ")
|
||||
)
|
||||
.replace(
|
||||
/{path}/g,
|
||||
req.url
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
)
|
||||
.replace(
|
||||
/{server}/g,
|
||||
"" +
|
||||
(
|
||||
config.generateServerString() +
|
||||
(!config.exposeModsInErrorPages || extName == undefined
|
||||
? ""
|
||||
: " " + extName)
|
||||
)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">") +
|
||||
(req.headers.host == undefined || req.isProxy
|
||||
? ""
|
||||
: " on " +
|
||||
String(req.headers.host)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">"))
|
||||
)
|
||||
.replace(
|
||||
/{contact}/g,
|
||||
config.serverAdministratorEmail
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.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;
|
|
@ -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) => {
|
||||
|
|
|
@ -854,7 +854,7 @@ if (!disableMods) {
|
|||
}
|
||||
|
||||
// Middleware
|
||||
let middleware = [
|
||||
const middleware = [
|
||||
require("./middleware/urlSanitizer.js"),
|
||||
require("./middleware/redirects.js"),
|
||||
require("./middleware/blocklist.js"),
|
||||
|
|
|
@ -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();
|
||||
|
|
22
src/utils/generateServerStringCore.js
Normal file
22
src/utils/generateServerStringCore.js
Normal 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
25
svrjs.core.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue