From d34d4bcc2d77e112ee9ba70d366d16c42473327f Mon Sep 17 00:00:00 2001 From: Dorian Niemiec Date: Sun, 10 Nov 2024 15:42:56 +0100 Subject: [PATCH] feat: add SVR.JS Core --- .gitignore | 1 + coreAssets/LICENSE | 21 + coreAssets/README.md | 3 + esbuild.config.js | 49 ++ package.json | 2 +- src/core.js | 633 ++++++++++++++++++++++++++ src/handlers/requestHandler.js | 6 +- src/index.js | 2 +- src/utils/generateServerStringCore.js | 20 + svrjs.core.json | 23 + 10 files changed, 755 insertions(+), 5 deletions(-) create mode 100644 coreAssets/LICENSE create mode 100644 coreAssets/README.md create mode 100644 src/core.js create mode 100644 src/utils/generateServerStringCore.js create mode 100644 svrjs.core.json 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( + `{errorMessage}

{errorMessage}

{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, "
") + .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(/{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") + ); + } 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) { + coreConfig = config; + return requestHandler; +} + +module.exports = init; +module.exports.init = init; diff --git a/src/handlers/requestHandler.js b/src/handlers/requestHandler.js index f501a2d..b2b6781 100644 --- a/src/handlers/requestHandler.js +++ b/src/handlers/requestHandler.js @@ -714,7 +714,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 +726,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 +736,7 @@ function requestHandler(req, res) { }; // Handle middleware - next(); + nextMiddleware(); } module.exports = (serverconsoleO, middlewareO) => { diff --git a/src/index.js b/src/index.js index a92b028..d3e313a 100644 --- a/src/index.js +++ b/src/index.js @@ -854,7 +854,7 @@ if (!disableMods) { } // Middleware -let middleware = [ +const middleware = [ require("./middleware/urlSanitizer.js"), require("./middleware/redirects.js"), require("./middleware/blocklist.js"), diff --git a/src/utils/generateServerStringCore.js b/src/utils/generateServerStringCore.js new file mode 100644 index 0000000..7d8a227 --- /dev/null +++ b/src/utils/generateServerStringCore.js @@ -0,0 +1,20 @@ +const svrjsInfo = require("../../svrjs.core.json"); +const { version, name } = 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; diff --git a/svrjs.core.json b/svrjs.core.json new file mode 100644 index 0000000..22b8984 --- /dev/null +++ b/svrjs.core.json @@ -0,0 +1,23 @@ +{ + "version": "Nightly-GitNext", + "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" + } +}