refactor: rewrite GreenRhombus to be a SVR.JS 4.x mod
This commit is contained in:
parent
650d040994
commit
c6559131c9
21 changed files with 8773 additions and 1006 deletions
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Build output
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# SVR.JS
|
||||||
|
/svrjs/
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# ESLint cache
|
||||||
|
.eslintcache
|
2
.husky/commit-msg
Executable file
2
.husky/commit-msg
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
npx --no -- commitlint --edit "$1"
|
2
.husky/pre-commit
Executable file
2
.husky/pre-commit
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
npx lint-staged
|
21
LICENSE
Normal file
21
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.
|
29
README
Normal file
29
README
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
This repository contains SVR.JS mod starter code and its build system
|
||||||
|
The mod will work for SVR.JS Nightly-GitNext.
|
||||||
|
|
||||||
|
Before doing anything, run "npm install".
|
||||||
|
To build SVR.JS mod, run "npm run build".
|
||||||
|
To check SVR.JS mod code for errors with ESLint, run "npm run lint".
|
||||||
|
To fix and beautify SVR.JS mod code with ESLint and Prettier, run "npm run lint:fix".
|
||||||
|
To perform unit tests with Jest, run "npm test".
|
||||||
|
|
||||||
|
To test the mod:
|
||||||
|
1. Clone the SVR.JS repository with "git clone https://git.svrjs.org/svrjs/svrjs.git" command.
|
||||||
|
2. Change the working directory to "svrjs" using "cd svrjs".
|
||||||
|
3. Build SVR.JS by first running "npm install" and then running "npm run build".
|
||||||
|
4. Copy the mod into mods directory in the dist directory using "cp ../dist/mod.js dist/mods" (GNU/Linux, Unix, BSD) or "copy ..\dist\mod.js dist\mods" (Windows).
|
||||||
|
5. Do the necessary mod configuration if the mod requires it.
|
||||||
|
6. Run SVR.JS by running "npm start".
|
||||||
|
7. Do some requests to the endpoints covered by the mod.
|
||||||
|
|
||||||
|
Structure:
|
||||||
|
- dist - contains the built SVR.JS mod
|
||||||
|
- src - contains SVR.JS mod source code
|
||||||
|
- index.js - entry point
|
||||||
|
- utils - utility functions
|
||||||
|
- tests - Jest unit tests
|
||||||
|
- utils - unit tests for utility functions
|
||||||
|
- esbuild.config.js - the build script
|
||||||
|
- eslint.config.js - ESLint configuration
|
||||||
|
- jest.config.js - Jest configuration
|
||||||
|
- modInfo.json - SVR.JS mod name and version
|
3
commitlint.config.js
Normal file
3
commitlint.config.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: ["@commitlint/config-conventional"]
|
||||||
|
};
|
11
esbuild.config.js
Normal file
11
esbuild.config.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
const esbuild = require("esbuild");
|
||||||
|
|
||||||
|
esbuild.build({
|
||||||
|
entryPoints: ["src/index.js"],
|
||||||
|
bundle: true,
|
||||||
|
outfile: "dist/mod.js", // Mod output file
|
||||||
|
platform: "node",
|
||||||
|
target: "es2017",
|
||||||
|
}).catch((err) => {
|
||||||
|
throw err;
|
||||||
|
});
|
30
eslint.config.js
Normal file
30
eslint.config.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
const globals = require("globals");
|
||||||
|
const pluginJs = require("@eslint/js");
|
||||||
|
const eslintPluginPrettierRecommended = require("eslint-plugin-prettier/recommended");
|
||||||
|
const jest = require("eslint-plugin-jest");
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
files: ["**/*.js"],
|
||||||
|
languageOptions: {
|
||||||
|
sourceType: "commonjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["tests/*.test.js", "tests/**/*.test.js"],
|
||||||
|
...jest.configs['flat/recommended'],
|
||||||
|
rules: {
|
||||||
|
...jest.configs['flat/recommended'].rules,
|
||||||
|
'jest/prefer-expect-assertions': 'off',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pluginJs.configs.recommended,
|
||||||
|
eslintPluginPrettierRecommended
|
||||||
|
];
|
5
jest.config.js
Normal file
5
jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testMatch: ['**/tests/**/*.test.js'],
|
||||||
|
verbose: true,
|
||||||
|
};
|
4
lint-staged.config.js
Normal file
4
lint-staged.config.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = {
|
||||||
|
"tests/**/*.js": "eslint --cache --fix",
|
||||||
|
"src/**/*.js": "eslint --cache --fix"
|
||||||
|
};
|
4
mod.info
4
mod.info
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"name": "GreenRhombus FastCGI client for SVR.JS",
|
|
||||||
"version": "Nightly-GitMain"
|
|
||||||
}
|
|
4
modInfo.json
Normal file
4
modInfo.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"name": "Example mod",
|
||||||
|
"version": "0.0.0"
|
||||||
|
}
|
7308
package-lock.json
generated
Normal file
7308
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
35
package.json
Normal file
35
package.json
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "svrjs-mod-starter",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "node esbuild.config.js",
|
||||||
|
"cz": "cz",
|
||||||
|
"lint": "eslint --no-error-on-unmatched-pattern src/**/*.js src/*.js tests/**/*.test.js tests/**/*.js",
|
||||||
|
"lint:fix": "npm run lint -- --fix",
|
||||||
|
"prepare": "husky",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^19.6.1",
|
||||||
|
"@commitlint/config-conventional": "^19.6.0",
|
||||||
|
"@eslint/js": "^9.9.1",
|
||||||
|
"commitizen": "^4.3.1",
|
||||||
|
"cz-conventional-changelog": "^3.3.0",
|
||||||
|
"esbuild": "^0.23.1",
|
||||||
|
"eslint": "^9.9.1",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-jest": "^28.8.0",
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"lint-staged": "^15.2.11",
|
||||||
|
"prettier": "^3.3.3"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"commitizen": {
|
||||||
|
"path": "./node_modules/cz-conventional-changelog"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
prettier.config.js
Normal file
15
prettier.config.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// prettier.config.js, .prettierrc.js, prettier.config.cjs, or .prettierrc.cjs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://prettier.io/docs/en/configuration.html
|
||||||
|
* @type {import("prettier").Config}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
trailingComma: "none",
|
||||||
|
tabWidth: 2,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: false,
|
||||||
|
endOfLine: "lf"
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
542
src/index.js
Normal file
542
src/index.js
Normal file
|
@ -0,0 +1,542 @@
|
||||||
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
|
const path = require("path");
|
||||||
|
const { FastCGIHandler } = require("./utils/fastCgi.js"); // FastCGI handler
|
||||||
|
const createCGIParser = require("./utils/cgiParser.js"); // CGI parser
|
||||||
|
const modInfo = require("../modInfo.json"); // SVR.JS mod information
|
||||||
|
|
||||||
|
let fastcgiConfigFromSeparateFile = {};
|
||||||
|
try {
|
||||||
|
fastcgiConfigFromSeparateFile = JSON.parse(
|
||||||
|
fs.readFileSync(process.dirname + "/greenrhombus-config.json")
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line
|
||||||
|
} catch (ex) {
|
||||||
|
// Use defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
let scriptExtsFromSeparateFile = [];
|
||||||
|
try {
|
||||||
|
scriptExtsFromSeparateFile = JSON.parse(
|
||||||
|
fs.readFileSync(process.dirname + "/greenrhombus-scriptexts.json")
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line
|
||||||
|
} catch (ex) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const executeFastCGI = (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
logFacilities,
|
||||||
|
defaultHeaders,
|
||||||
|
customEnv,
|
||||||
|
fastCGIConfig
|
||||||
|
) => {
|
||||||
|
const environment = Object.assign({}, process.env, customEnv);
|
||||||
|
let errorBuffer = "";
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
host: fastCGIConfig.host,
|
||||||
|
port: fastCGIConfig.port,
|
||||||
|
path: fastCGIConfig.socketPath,
|
||||||
|
env: environment
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = new FastCGIHandler(options);
|
||||||
|
|
||||||
|
const handleData = createCGIParser(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
handler,
|
||||||
|
defaultHeaders,
|
||||||
|
`GreenRhombus/${modInfo.version}`
|
||||||
|
);
|
||||||
|
|
||||||
|
handler.on("error", (error) => {
|
||||||
|
if (handler.headersParsed) return; // Don't call res.writeHead multiple times
|
||||||
|
const errorCode = ["ENOTFOUND", "EHOSTUNREACH", "ECONNREFUSED"].includes(
|
||||||
|
error.code
|
||||||
|
)
|
||||||
|
? 503
|
||||||
|
: error.code === "EMFILE"
|
||||||
|
? 429
|
||||||
|
: 500;
|
||||||
|
res.error(errorCode, `GreenRhombus/${modInfo.version}`, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.prependListener("close", () => {
|
||||||
|
if (handler.stdout) handler.stdout.unpipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on("error", () => {}); // Suppress response stream errors
|
||||||
|
|
||||||
|
const handleConnection = () => {
|
||||||
|
handler.stdout.on("data", handleData);
|
||||||
|
handler.stderr.on("data", (data) => {
|
||||||
|
errorBuffer += data.toString();
|
||||||
|
});
|
||||||
|
handler.init();
|
||||||
|
req.pipe(handler.stdin);
|
||||||
|
|
||||||
|
handler.on("exit", (exitCode, signal) => {
|
||||||
|
if (!handler.headersParsed && (signal || exitCode !== 0)) {
|
||||||
|
const error = new Error(
|
||||||
|
`Process execution failed!${errorBuffer ? ` Reason: ${errorBuffer.trim()}` : ""}`
|
||||||
|
);
|
||||||
|
res.error(500, `GreenRhombus/${modInfo.version}`, error);
|
||||||
|
} else if (errorBuffer.trim()) {
|
||||||
|
logFacilities.errmessage("There were FastCGI application errors:");
|
||||||
|
logFacilities.errmessage(errorBuffer.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.stdout.removeListener("data", handleData);
|
||||||
|
handler.stdout.unpipe(res);
|
||||||
|
if (!res.finished) res.end();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (handler.socket.connecting === undefined || handler.socket.connecting) {
|
||||||
|
handler.on("connect", handleConnection);
|
||||||
|
} else if (!handler.socket.destroyed) {
|
||||||
|
handleConnection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function executeFastCGIWithEnv(
|
||||||
|
scriptPath,
|
||||||
|
pathInfo,
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
logFacilities,
|
||||||
|
serverSoftware,
|
||||||
|
defaultHeaders,
|
||||||
|
fastCGIConfig,
|
||||||
|
wwwroot,
|
||||||
|
svrjsConfig
|
||||||
|
) {
|
||||||
|
// Function to set up environment variables and execute FastCGI scripts
|
||||||
|
const environmentVariables = {};
|
||||||
|
|
||||||
|
// Authentication details
|
||||||
|
if (req.authUser) {
|
||||||
|
if (req.headers.authorization) {
|
||||||
|
environmentVariables.AUTH_TYPE = req.headers.authorization.split(" ")[0];
|
||||||
|
}
|
||||||
|
environmentVariables.REMOTE_USER = req.authUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query string
|
||||||
|
const queryString = req.url.split("?")[1] || "";
|
||||||
|
environmentVariables.QUERY_STRING = queryString;
|
||||||
|
|
||||||
|
// Server details
|
||||||
|
environmentVariables.SERVER_SOFTWARE = serverSoftware;
|
||||||
|
environmentVariables.SERVER_PROTOCOL = `HTTP/${req.httpVersion}`;
|
||||||
|
|
||||||
|
if (req.socket.localAddress && req.socket.localPort) {
|
||||||
|
environmentVariables.SERVER_PORT = req.socket.localPort;
|
||||||
|
let localAddress = req.socket.localAddress.replace(/^::ffff:/i, "");
|
||||||
|
if (localAddress.includes(":")) {
|
||||||
|
localAddress = `[${localAddress}]`;
|
||||||
|
}
|
||||||
|
environmentVariables.SERVER_ADDR = localAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
svrjsConfig.serverAdministratorEmail &&
|
||||||
|
svrjsConfig.serverAdministratorEmail !== "[no contact information]"
|
||||||
|
) {
|
||||||
|
environmentVariables.SERVER_ADMIN = svrjsConfig.serverAdministratorEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
environmentVariables.SERVER_NAME = req.headers.host;
|
||||||
|
environmentVariables.DOCUMENT_ROOT = wwwroot;
|
||||||
|
|
||||||
|
// Script details
|
||||||
|
const platform = os.platform();
|
||||||
|
const normalizePath = (path) =>
|
||||||
|
platform === "win32" ? path.replace(/\//g, "\\") : path;
|
||||||
|
|
||||||
|
environmentVariables.SCRIPT_NAME = scriptPath;
|
||||||
|
environmentVariables.SCRIPT_FILENAME = normalizePath(
|
||||||
|
`${wwwroot}${scriptPath}`
|
||||||
|
)
|
||||||
|
.replace(/\\+/g, "\\")
|
||||||
|
.replace(/\/\/+/, "/");
|
||||||
|
environmentVariables.PATH_INFO = decodeURIComponent(pathInfo);
|
||||||
|
environmentVariables.PATH_TRANSLATED = pathInfo
|
||||||
|
? normalizePath(`${wwwroot}${decodeURIComponent(pathInfo)}`)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// Request details
|
||||||
|
environmentVariables.REQUEST_METHOD = req.method;
|
||||||
|
environmentVariables.REQUEST_URI =
|
||||||
|
req.originalParsedURL &&
|
||||||
|
req.parsedURL &&
|
||||||
|
req.originalParsedURL.pathname === req.parsedURL.pathname
|
||||||
|
? req.url
|
||||||
|
: `${req.originalParsedURL.pathname}${req.parsedURL.search ? `?${req.parsedURL.search}` : ""}`;
|
||||||
|
|
||||||
|
const remoteAddress =
|
||||||
|
req.socket.realRemoteAddress ||
|
||||||
|
(svrjsConfig.enableIPSpoofing &&
|
||||||
|
req.headers["x-forwarded-for"] &&
|
||||||
|
req.headers["x-forwarded-for"].split(",")[0].trim()) ||
|
||||||
|
req.socket.remoteAddress;
|
||||||
|
|
||||||
|
environmentVariables.REMOTE_ADDR = remoteAddress.replace(/^::ffff:/i, "");
|
||||||
|
environmentVariables.REMOTE_PORT =
|
||||||
|
req.socket.realRemotePort || req.socket.remotePort;
|
||||||
|
|
||||||
|
if (req.socket.encrypted) {
|
||||||
|
environmentVariables.HTTPS = "ON";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content headers
|
||||||
|
if (req.headers["content-type"]) {
|
||||||
|
environmentVariables.CONTENT_TYPE = req.headers["content-type"];
|
||||||
|
}
|
||||||
|
|
||||||
|
environmentVariables.CONTENT_LENGTH = req.headers["content-length"] || "0";
|
||||||
|
|
||||||
|
// HTTP headers
|
||||||
|
const headersCopy = Object.assign({}, req.headers);
|
||||||
|
delete headersCopy["content-type"];
|
||||||
|
delete headersCopy["content-length"];
|
||||||
|
|
||||||
|
Object.keys(headersCopy).forEach((key) => {
|
||||||
|
const normalizedKey = key.replace(/[^0-9A-Za-z]+/g, "_").toUpperCase();
|
||||||
|
environmentVariables[`HTTP_${normalizedKey}`] = headersCopy[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
executeFastCGI(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
logFacilities,
|
||||||
|
defaultHeaders,
|
||||||
|
environmentVariables,
|
||||||
|
fastCGIConfig
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedWebrootSupported =
|
||||||
|
process.versions.svrjs &&
|
||||||
|
process.versions.svrjs.match(
|
||||||
|
/^(?:Nightly-|(?:[5-9]|[1234][0-9])[0-9]*\.|4\.(?:(?:[1][0-9]|[2-9])+\.))/i
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exported SVR.JS mod callback
|
||||||
|
module.exports = (req, res, logFacilities, config, next) => {
|
||||||
|
let scriptExts = config.fastCGIScriptExtensions
|
||||||
|
? config.fastCGIScriptExtensions
|
||||||
|
: scriptExtsFromSeparateFile;
|
||||||
|
let fastCGIConfig = config.fastCGIConfiguration
|
||||||
|
? config.fastCGIConfiguration
|
||||||
|
: fastcgiConfigFromSeparateFile;
|
||||||
|
const detectedWwwroot = normalizedWebrootSupported
|
||||||
|
? config.wwwroot
|
||||||
|
: process.cwd();
|
||||||
|
|
||||||
|
let ext = req.parsedURL.pathname.match(/[^/]\.([^.]+)$/);
|
||||||
|
if (!ext) ext = "";
|
||||||
|
else ext = ext[1].toLowerCase();
|
||||||
|
|
||||||
|
const isScriptExt = scriptExts.indexOf("." + ext) != -1;
|
||||||
|
|
||||||
|
let preparedFastCGIConfig = {};
|
||||||
|
if (fastCGIConfig.multiConfig) {
|
||||||
|
const hostnames = Object.keys(fastCGIConfig.multiConfig);
|
||||||
|
for (let i = 0; i < hostnames.length; i++) {
|
||||||
|
if (hostnames[i] == "*") {
|
||||||
|
preparedFastCGIConfig = Object.assign(
|
||||||
|
{},
|
||||||
|
fastCGIConfig.multiConfig["*"]
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
} else if (
|
||||||
|
req.headers.host &&
|
||||||
|
hostnames[i].indexOf("*.") == 0 &&
|
||||||
|
hostnames[i] != "*."
|
||||||
|
) {
|
||||||
|
const hostnamesRoot = hostnames[i].substring(2);
|
||||||
|
if (
|
||||||
|
req.headers.host == hostnamesRoot ||
|
||||||
|
(req.headers.host.length > hostnamesRoot.length &&
|
||||||
|
req.headers.host.indexOf("." + hostnamesRoot) ==
|
||||||
|
req.headers.host.length - hostnamesRoot.length - 1)
|
||||||
|
) {
|
||||||
|
preparedFastCGIConfig = Object.assign(
|
||||||
|
{},
|
||||||
|
fastCGIConfig.multiConfig[hostnames[i]]
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (req.headers.host && req.headers.host == hostnames[i]) {
|
||||||
|
preparedFastCGIConfig = Object.assign(
|
||||||
|
{},
|
||||||
|
fastCGIConfig.multiConfig[hostnames[i]]
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
preparedFastCGIConfig = Object.assign({}, fastCGIConfig);
|
||||||
|
delete preparedFastCGIConfig.multiConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preparedFastCGIConfig.path !== undefined)
|
||||||
|
preparedFastCGIConfig.path = preparedFastCGIConfig.path.replace(
|
||||||
|
/([^/])\/+$/,
|
||||||
|
"$1"
|
||||||
|
);
|
||||||
|
if (preparedFastCGIConfig.socketPath === undefined) {
|
||||||
|
if (preparedFastCGIConfig.host === undefined)
|
||||||
|
preparedFastCGIConfig.host = "localhost";
|
||||||
|
if (preparedFastCGIConfig.port === undefined)
|
||||||
|
preparedFastCGIConfig.port = 4000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
preparedFastCGIConfig.path !== undefined &&
|
||||||
|
(req.parsedURL.pathname == preparedFastCGIConfig.path ||
|
||||||
|
req.parsedURL.pathname.indexOf(preparedFastCGIConfig.path + "/") == 0)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
executeFastCGIWithEnv(
|
||||||
|
decodeURIComponent(req.parsedURL.pathname),
|
||||||
|
req.parsedURL.pathname.replace(preparedFastCGIConfig.path, ""),
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
logFacilities,
|
||||||
|
config.getCustomHeaders()["Server"] +
|
||||||
|
(config.exposeModsInErrorPages ||
|
||||||
|
config.exposeModsInErrorPages === undefined
|
||||||
|
? " GreenRhombus/" + modInfo.version
|
||||||
|
: ""),
|
||||||
|
config.getCustomHeaders(),
|
||||||
|
preparedFastCGIConfig,
|
||||||
|
detectedWwwroot,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
} catch (ex) {
|
||||||
|
res.error(500, `GreenRhombus/${modInfo.version}`, ex);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
((req.parsedURL.pathname == "/greenrhombus-config.json" ||
|
||||||
|
(os.platform() == "win32" &&
|
||||||
|
req.parsedURL.pathname.toLowerCase() == "/greenrhombus-config.json")) &&
|
||||||
|
path.normalize(__dirname + "/../../..") == detectedWwwroot) ||
|
||||||
|
((req.parsedURL.pathname == "/greenrhombus-scriptexts.json" ||
|
||||||
|
(os.platform() == "win32" &&
|
||||||
|
req.parsedURL.pathname.toLowerCase() ==
|
||||||
|
"/greenrhombus-scriptexts.json")) &&
|
||||||
|
path.normalize(__dirname + "/../../..") == detectedWwwroot)
|
||||||
|
) {
|
||||||
|
res.error(403, `GreenRhombus/${modInfo.version}`);
|
||||||
|
} else {
|
||||||
|
fs.stat(
|
||||||
|
detectedWwwroot + decodeURIComponent(req.parsedURL.pathname),
|
||||||
|
(err, stats) => {
|
||||||
|
if (!err) {
|
||||||
|
if (!stats.isFile()) {
|
||||||
|
if (scriptExts.indexOf(".php") != -1) {
|
||||||
|
fs.stat(
|
||||||
|
detectedWwwroot +
|
||||||
|
decodeURIComponent(req.parsedURL.pathname) +
|
||||||
|
"/index.php",
|
||||||
|
(e2, s2) => {
|
||||||
|
if (!e2 && s2.isFile()) {
|
||||||
|
try {
|
||||||
|
executeFastCGIWithEnv(
|
||||||
|
(
|
||||||
|
decodeURIComponent(req.parsedURL.pathname) +
|
||||||
|
"/index.php"
|
||||||
|
).replace(/\/+/g, "/"),
|
||||||
|
"",
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
logFacilities,
|
||||||
|
config.getCustomHeaders()["Server"] +
|
||||||
|
(config.exposeModsInErrorPages ||
|
||||||
|
config.exposeModsInErrorPages === undefined
|
||||||
|
? " GreenRhombus/" + modInfo.version
|
||||||
|
: ""),
|
||||||
|
config.getCustomHeaders(),
|
||||||
|
preparedFastCGIConfig,
|
||||||
|
detectedWwwroot,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
} catch (ex) {
|
||||||
|
res.error(500, `GreenRhombus/${modInfo.version}`, ex);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (scriptExts.indexOf(".cgi") != -1) {
|
||||||
|
fs.stat(
|
||||||
|
detectedWwwroot +
|
||||||
|
decodeURIComponent(req.parsedURL.pathname) +
|
||||||
|
"/index.cgi",
|
||||||
|
(e3, s3) => {
|
||||||
|
if (!e3 && s3.isFile()) {
|
||||||
|
try {
|
||||||
|
executeFastCGIWithEnv(
|
||||||
|
(
|
||||||
|
decodeURIComponent(req.parsedURL.pathname) +
|
||||||
|
"/index.cgi"
|
||||||
|
).replace(/\/+/g, "/"),
|
||||||
|
"",
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
logFacilities,
|
||||||
|
config.getCustomHeaders()["Server"] +
|
||||||
|
(config.exposeModsInErrorPages ||
|
||||||
|
config.exposeModsInErrorPages === undefined
|
||||||
|
? " GreenRhombus/" + modInfo.version
|
||||||
|
: ""),
|
||||||
|
config.getCustomHeaders(),
|
||||||
|
preparedFastCGIConfig,
|
||||||
|
detectedWwwroot,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
} catch (ex) {
|
||||||
|
res.error(500, `GreenRhombus/${modInfo.version}`, ex);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isScriptExt) {
|
||||||
|
try {
|
||||||
|
executeFastCGIWithEnv(
|
||||||
|
decodeURIComponent(req.parsedURL.pathname),
|
||||||
|
"",
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
logFacilities,
|
||||||
|
config.getCustomHeaders()["Server"] +
|
||||||
|
(config.exposeModsInErrorPages ||
|
||||||
|
config.exposeModsInErrorPages === undefined
|
||||||
|
? " GreenRhombus/" + modInfo.version
|
||||||
|
: ""),
|
||||||
|
config.getCustomHeaders(),
|
||||||
|
preparedFastCGIConfig,
|
||||||
|
detectedWwwroot,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
} catch (ex) {
|
||||||
|
res.error(500, `GreenRhombus/${modInfo.version}`, ex);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (err && err.code == "ENOTDIR") {
|
||||||
|
const checkPath = (pth, cb, a) => {
|
||||||
|
// Function to check the path of the file and connect to the FastCGI server
|
||||||
|
const cpth = pth.split("/");
|
||||||
|
if (cpth.length < 2) {
|
||||||
|
cb(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let b = [];
|
||||||
|
if (a) b = a.split("/");
|
||||||
|
fs.stat(detectedWwwroot + "/" + pth, function (err, stats) {
|
||||||
|
if (!err && stats.isFile()) {
|
||||||
|
cb({
|
||||||
|
scriptPath: pth,
|
||||||
|
pathInfo: a !== undefined ? "/" + a : ""
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
b.unshift(cpth.pop());
|
||||||
|
return checkPath(cpth.join("/"), cb, b.join("/"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
checkPath(
|
||||||
|
"." + decodeURIComponent(req.parsedURL.pathname),
|
||||||
|
function (pathp) {
|
||||||
|
if (!pathp) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
let newext = pathp.scriptPath.match(/[^/]\.([^.]+)$/);
|
||||||
|
if (!newext) newext = "";
|
||||||
|
else newext = newext[1].toLowerCase();
|
||||||
|
if (scriptExts.indexOf(newext) != -1) {
|
||||||
|
try {
|
||||||
|
executeFastCGIWithEnv(
|
||||||
|
pathp.scriptPath.substring(1),
|
||||||
|
pathp.pathInfo,
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
logFacilities,
|
||||||
|
config.getCustomHeaders()["Server"] +
|
||||||
|
(config.exposeModsInErrorPages ||
|
||||||
|
config.exposeModsInErrorPages === undefined
|
||||||
|
? " GreenRhombus/" + modInfo.version
|
||||||
|
: ""),
|
||||||
|
config.getCustomHeaders(),
|
||||||
|
preparedFastCGIConfig,
|
||||||
|
detectedWwwroot,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
} catch (ex) {
|
||||||
|
res.error(500, `GreenRhombus/${modInfo.version}`, ex);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
next(); //Invoke default error handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.configValidators = {
|
||||||
|
fastCGIConfiguration: (value) =>
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
(value.path === undefined || typeof value.path === "string") &&
|
||||||
|
(value.socketPath === undefined || typeof value.socketPath === "string") &&
|
||||||
|
(value.host === undefined || typeof value.host === "string") &&
|
||||||
|
(value.port === undefined ||
|
||||||
|
(typeof value.port === "number" &&
|
||||||
|
value.port >= 0 &&
|
||||||
|
value.port <= 65535)) &&
|
||||||
|
(value.multiConfig === undefined ||
|
||||||
|
(typeof value.multiConfig === "object" &&
|
||||||
|
value.multiConfig !== null &&
|
||||||
|
Object.keys(value.multiConfig).every((host) => {
|
||||||
|
const hostValue = value.multiConfig[host];
|
||||||
|
return (
|
||||||
|
(hostValue.path === undefined ||
|
||||||
|
typeof hostValue.path === "string") &&
|
||||||
|
(hostValue.socketPath === undefined ||
|
||||||
|
typeof hostValue.socketPath === "string") &&
|
||||||
|
(hostValue.host === undefined ||
|
||||||
|
typeof hostValue.host === "string") &&
|
||||||
|
(hostValue.port === undefined ||
|
||||||
|
(typeof hostValue.port === "number" &&
|
||||||
|
hostValue.port >= 0 &&
|
||||||
|
hostValue.port <= 65535))
|
||||||
|
);
|
||||||
|
}))),
|
||||||
|
|
||||||
|
fastCGIScriptExtensions: (value) =>
|
||||||
|
Array.isArray(value) && value.every((ext) => typeof ext === "string")
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.modInfo = modInfo;
|
101
src/utils/cgiParser.js
Normal file
101
src/utils/cgiParser.js
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
function createCGIParser(req, res, handler, defaultHeaders, software) {
|
||||||
|
let responseBuffer = "";
|
||||||
|
let headerEndIndex = -1;
|
||||||
|
|
||||||
|
const dataHandler = (data) => {
|
||||||
|
if (!dataHandler.headersParsed) {
|
||||||
|
responseBuffer += data.toString("latin1");
|
||||||
|
const headerMatch = responseBuffer.match(
|
||||||
|
/(?:\r\n\r\n|\n\r\n\r|\n\n|\r\r)/
|
||||||
|
);
|
||||||
|
|
||||||
|
if (headerMatch) {
|
||||||
|
dataHandler.headersParsed = true;
|
||||||
|
const endOfHeaders = headerMatch[0];
|
||||||
|
headerEndIndex = headerMatch.index;
|
||||||
|
|
||||||
|
const rawHeaders = responseBuffer
|
||||||
|
.substring(0, headerEndIndex)
|
||||||
|
.split(/(?:\r\n|\n\r|\n|\r)/);
|
||||||
|
|
||||||
|
let parsedHeaders = defaultHeaders || {};
|
||||||
|
let statusCode = 200;
|
||||||
|
let statusMessage = "OK";
|
||||||
|
|
||||||
|
if (rawHeaders[0].match(/^HTTP\//)) {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const [ignore, code, message] =
|
||||||
|
rawHeaders.shift().match(/^[^ ]+ *([0-9]{3}) *(.*)/) || [];
|
||||||
|
if (code) {
|
||||||
|
statusCode = parseInt(code);
|
||||||
|
statusMessage = message;
|
||||||
|
}
|
||||||
|
} else if (rawHeaders[0].indexOf(":") === -1) {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const [ignore, code, message] =
|
||||||
|
rawHeaders.shift().match(/^([0-9]{3}) *(.*)/) || [];
|
||||||
|
if (code) {
|
||||||
|
statusCode = parseInt(code);
|
||||||
|
statusMessage = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasLocationHeader = false;
|
||||||
|
|
||||||
|
rawHeaders.forEach((headerLine) => {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const [ignore, name, value] =
|
||||||
|
headerLine.match(/^([^:]+): *(.*)/) || [];
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
if (name.toLowerCase() === "status") {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const [ignore, code, message] =
|
||||||
|
value.match(/^([0-9]{3}) *(.*)/) || [];
|
||||||
|
if (code) {
|
||||||
|
statusCode = parseInt(code);
|
||||||
|
statusMessage = message;
|
||||||
|
}
|
||||||
|
} else if (name.toLowerCase() === "set-cookie") {
|
||||||
|
parsedHeaders["Set-Cookie"] = parsedHeaders["Set-Cookie"] || [];
|
||||||
|
parsedHeaders["Set-Cookie"].push(value);
|
||||||
|
} else {
|
||||||
|
if (name.toLowerCase() === "location") hasLocationHeader = true;
|
||||||
|
parsedHeaders[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ((statusCode < 300 || statusCode > 399) && hasLocationHeader) {
|
||||||
|
statusCode = 302;
|
||||||
|
statusMessage = "Found";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.writeHead(statusCode, statusMessage, parsedHeaders);
|
||||||
|
res.write(
|
||||||
|
Buffer.from(
|
||||||
|
responseBuffer.substring(headerEndIndex + endOfHeaders.length),
|
||||||
|
"latin1"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
handler.removeAllListeners("exit");
|
||||||
|
handler.stdout.removeAllListeners("data");
|
||||||
|
res.error(500, software, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!res.finished) {
|
||||||
|
res.write(data);
|
||||||
|
handler.stdout.removeListener("data", dataHandler);
|
||||||
|
handler.stdout.pipe(res, { end: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dataHandler.headersParsed = false;
|
||||||
|
|
||||||
|
return dataHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createCGIParser;
|
370
src/utils/fastCgi.js
Normal file
370
src/utils/fastCgi.js
Normal file
|
@ -0,0 +1,370 @@
|
||||||
|
const EventEmitter = require("events");
|
||||||
|
const stream = require("stream");
|
||||||
|
const net = require("net");
|
||||||
|
|
||||||
|
const constants = {
|
||||||
|
BEGIN_REQUEST: 1,
|
||||||
|
ABORT_REQUEST: 2,
|
||||||
|
END_REQUEST: 3,
|
||||||
|
PARAMS: 4,
|
||||||
|
STDIN: 5,
|
||||||
|
STDOUT: 6,
|
||||||
|
STDERR: 7,
|
||||||
|
DATA: 8,
|
||||||
|
GET_VALUES: 9,
|
||||||
|
GET_VALUES_RESULT: 10,
|
||||||
|
UNKNOWN_TYPE: 11,
|
||||||
|
RESPONDER: 1,
|
||||||
|
REQUEST_COMPLETE: 0,
|
||||||
|
CANT_MPX_CONN: 1,
|
||||||
|
OVERLOADED: 2,
|
||||||
|
UNKNOWN_ROLE: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildFastCGIPacket(type, requestID, content) {
|
||||||
|
const packet = Buffer.alloc(content.length + (content.length % 8) + 8);
|
||||||
|
packet.writeUInt8(1, 0); // version
|
||||||
|
packet.writeUInt8(type, 1); // type
|
||||||
|
packet.writeUInt16BE(requestID, 2); // requestId
|
||||||
|
packet.writeUInt16BE(content.length, 4); // contentLength
|
||||||
|
packet.writeUInt8(content.length % 8, 6); // paddingLength
|
||||||
|
packet.writeUInt8(0, 7); // reserved
|
||||||
|
Buffer.from(content).copy(packet, 8); // content
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNameValuePair(name, value) {
|
||||||
|
if (typeof name == "undefined" || typeof value == "undefined")
|
||||||
|
return Buffer.alloc(0);
|
||||||
|
name = String(name);
|
||||||
|
value = String(value);
|
||||||
|
const nameLength = name.length;
|
||||||
|
const valueLength = value.length;
|
||||||
|
const nameLengthBytes = nameLength > 127 ? 4 : 1;
|
||||||
|
const valueLengthBytes = valueLength > 127 ? 4 : 1;
|
||||||
|
const pair = Buffer.alloc(
|
||||||
|
nameLength + valueLength + nameLengthBytes + valueLengthBytes
|
||||||
|
);
|
||||||
|
|
||||||
|
// nameLength
|
||||||
|
if (nameLengthBytes == 4) {
|
||||||
|
pair.writeUInt32BE(Math.min(0xffffffff, 0x80000000 + nameLength), 0);
|
||||||
|
} else {
|
||||||
|
pair.writeUInt8(nameLength, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// valueLength
|
||||||
|
if (valueLengthBytes == 4) {
|
||||||
|
pair.writeUInt32BE(
|
||||||
|
Math.min(0xffffffff, 0x80000000 + valueLength),
|
||||||
|
nameLengthBytes
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
pair.writeUInt8(valueLength, nameLengthBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer.from(name).copy(pair, nameLengthBytes + valueLengthBytes); // nameData
|
||||||
|
Buffer.from(value).copy(
|
||||||
|
pair,
|
||||||
|
nameLengthBytes + valueLengthBytes + nameLength
|
||||||
|
); // valueData
|
||||||
|
|
||||||
|
return pair;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFastCGIPacket(socket, type, requestID, content) {
|
||||||
|
if (content.length != 0) {
|
||||||
|
const contentSlices = Math.ceil(content.length / 65535);
|
||||||
|
let chunk = Buffer.alloc(0);
|
||||||
|
for (let i = 0; i < contentSlices; i++) {
|
||||||
|
let chunkOffset = 65536 * i;
|
||||||
|
const chunkSize = Math.min(65535, content.length - chunkOffset);
|
||||||
|
chunk = Buffer.alloc(chunkSize);
|
||||||
|
content.copy(chunk, 0, chunkOffset, chunkOffset + chunkSize);
|
||||||
|
socket.write(buildFastCGIPacket(type, requestID, chunk));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
socket.write(buildFastCGIPacket(type, requestID, Buffer.alloc(0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFastCGIPacket(packet) {
|
||||||
|
return {
|
||||||
|
fcgiVersion: packet.readUInt8(0), // version
|
||||||
|
type: packet.readUInt8(1), // type
|
||||||
|
requestID: packet.readUInt16BE(2), // requestId
|
||||||
|
content: packet.subarray(8, 8 + packet.readUInt16BE(4)) // contentLength
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function FastCGIHandler(options) {
|
||||||
|
let requestID = options.requestID;
|
||||||
|
if (!requestID) requestID = 1;
|
||||||
|
|
||||||
|
const fastCGISocketHandler = (chunk) => {
|
||||||
|
let chunkIndex = 0;
|
||||||
|
|
||||||
|
while (
|
||||||
|
chunkIndex < chunk.length ||
|
||||||
|
(headerIndex == 8 && bodyIndex == packetBody.length && paddingLength == 0)
|
||||||
|
) {
|
||||||
|
if (headerIndex < 8) {
|
||||||
|
chunk.copy(
|
||||||
|
packetHeader,
|
||||||
|
headerIndex,
|
||||||
|
chunkIndex,
|
||||||
|
Math.min(chunk.length, chunkIndex + 8 - headerIndex)
|
||||||
|
);
|
||||||
|
const ic = Math.min(chunk.length - chunkIndex, 8 - headerIndex);
|
||||||
|
headerIndex += ic;
|
||||||
|
chunkIndex += ic;
|
||||||
|
if (headerIndex == 8) {
|
||||||
|
packetBody = Buffer.alloc(packetHeader.readUInt16BE(4));
|
||||||
|
paddingLength = packetHeader.readUInt8(6);
|
||||||
|
}
|
||||||
|
} else if (headerIndex == 8 && bodyIndex < packetBody.length) {
|
||||||
|
chunk.copy(
|
||||||
|
packetBody,
|
||||||
|
bodyIndex,
|
||||||
|
chunkIndex,
|
||||||
|
Math.min(chunk.length, chunkIndex + packetBody.length - bodyIndex)
|
||||||
|
);
|
||||||
|
const ic = Math.min(
|
||||||
|
chunk.length - chunkIndex,
|
||||||
|
packetBody.length - bodyIndex
|
||||||
|
);
|
||||||
|
bodyIndex += ic;
|
||||||
|
chunkIndex += ic;
|
||||||
|
} else if (
|
||||||
|
headerIndex == 8 &&
|
||||||
|
bodyIndex == packetBody.length &&
|
||||||
|
paddingIndex <= paddingLength
|
||||||
|
) {
|
||||||
|
const ic = Math.min(
|
||||||
|
chunk.length - chunkIndex,
|
||||||
|
paddingLength - paddingIndex
|
||||||
|
);
|
||||||
|
paddingIndex += ic;
|
||||||
|
chunkIndex += ic;
|
||||||
|
if (paddingIndex == paddingLength) {
|
||||||
|
headerIndex = 0;
|
||||||
|
bodyIndex = 0;
|
||||||
|
paddingIndex = 0;
|
||||||
|
const packet = Buffer.alloc(8 + packetBody.length + paddingLength);
|
||||||
|
packetHeader.copy(packet);
|
||||||
|
packetBody.copy(packet, 8);
|
||||||
|
processFastCGIPacket(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let processFastCGIPacket = (packet) => {
|
||||||
|
const processedPacket = parseFastCGIPacket(packet);
|
||||||
|
if (processedPacket.requestID != requestID) return; // Drop the packet
|
||||||
|
if (processedPacket.type == constants.STDOUT) {
|
||||||
|
try {
|
||||||
|
if (processedPacket.content.length > 0)
|
||||||
|
stdoutPush(processedPacket.content);
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
} catch (err) {
|
||||||
|
// STDOUT will be lost instead of crashing the server
|
||||||
|
}
|
||||||
|
} else if (processedPacket.type == constants.STDERR) {
|
||||||
|
try {
|
||||||
|
if (processedPacket.content.length > 0)
|
||||||
|
emulatedStderr.push(processedPacket.content);
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
} catch (err) {
|
||||||
|
// STDERR will be lost anyway...
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
processedPacket.type == constants.END_REQUEST &&
|
||||||
|
processedPacket.content.length > 5
|
||||||
|
) {
|
||||||
|
if (typeof socket !== "undefined") {
|
||||||
|
socket.removeListener("data", fastCGISocketHandler);
|
||||||
|
socket.removeAllListeners("error");
|
||||||
|
socket.addListener("error", () => {});
|
||||||
|
processFastCGIPacket = function () {};
|
||||||
|
try {
|
||||||
|
socket.end(); // Fixes connection not closing properly in Bun
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
} catch (err) {
|
||||||
|
// It is already closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const appStatus = processedPacket.content.readUInt32BE(0);
|
||||||
|
const protocolStatus = processedPacket.content.readUInt8(4);
|
||||||
|
if (protocolStatus != constants.REQUEST_COMPLETE) {
|
||||||
|
let err = new Error("Unknown error");
|
||||||
|
if (protocolStatus == constants.OVERLOADED) {
|
||||||
|
err = new Error("FastCGI server overloaded");
|
||||||
|
err.code = "EMFILE";
|
||||||
|
err.errno = 24;
|
||||||
|
} else if (protocolStatus == constants.UNKNOWN_ROLE) {
|
||||||
|
err = new Error("Role not supported by the FastCGI application");
|
||||||
|
} else if (protocolStatus == constants.CANT_MPX_CONN) {
|
||||||
|
err = new Error(
|
||||||
|
"Multiplexed connections not supported by the FastCGI application"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
stdoutPush(null);
|
||||||
|
if (
|
||||||
|
emulatedStdout._readableState &&
|
||||||
|
emulatedStdout._readableState.flowing !== null &&
|
||||||
|
!emulatedStdout.endEmitted
|
||||||
|
) {
|
||||||
|
emulatedStdout.on("end", function () {
|
||||||
|
emulatedStderr.push(null);
|
||||||
|
eventEmitter.emit("error", err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
emulatedStderr.push(null);
|
||||||
|
eventEmitter.emit("error", err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stdoutPush(null);
|
||||||
|
if (
|
||||||
|
emulatedStdout._readableState &&
|
||||||
|
emulatedStdout._readableState.flowing !== null &&
|
||||||
|
!emulatedStdout.endEmitted
|
||||||
|
) {
|
||||||
|
emulatedStdout.on("end", function () {
|
||||||
|
emulatedStderr.push(null);
|
||||||
|
eventEmitter.emit("exit", appStatus, null);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
emulatedStderr.push(null);
|
||||||
|
eventEmitter.emit("exit", appStatus, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventEmitter = new EventEmitter();
|
||||||
|
let packetHeader = Buffer.alloc(8);
|
||||||
|
let packetBody = Buffer.alloc(0);
|
||||||
|
let paddingLength = 0;
|
||||||
|
let headerIndex = 0;
|
||||||
|
let bodyIndex = 0;
|
||||||
|
let paddingIndex = 0;
|
||||||
|
|
||||||
|
const emulatedStdin = new stream.Writable({
|
||||||
|
write: function (chunk, encoding, callback) {
|
||||||
|
try {
|
||||||
|
if (chunk.length != 0) {
|
||||||
|
writeFastCGIPacket(socket, constants.STDIN, requestID, chunk);
|
||||||
|
}
|
||||||
|
callback(null);
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
final: function (callback) {
|
||||||
|
try {
|
||||||
|
writeFastCGIPacket(socket, constants.STDIN, requestID, Buffer.alloc(0));
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
} catch (err) {
|
||||||
|
// writing failed
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function stdoutPush(data) {
|
||||||
|
if (data === null) {
|
||||||
|
stdoutToEnd = true;
|
||||||
|
} else {
|
||||||
|
stdoutBuffer = Buffer.concat([stdoutBuffer, Buffer.from(data)]);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < hp.length; i++) {
|
||||||
|
const func = hp.shift();
|
||||||
|
if (func) func();
|
||||||
|
}
|
||||||
|
emulatedStdout.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdoutBuffer = Buffer.alloc(0);
|
||||||
|
let stdoutToEnd = false;
|
||||||
|
const hp = [];
|
||||||
|
const emulatedStdout = new stream.Readable({
|
||||||
|
read: (n) => {
|
||||||
|
const s = emulatedStdout;
|
||||||
|
const handler = () => {
|
||||||
|
if (stdoutBuffer.length == 0) {
|
||||||
|
if (!stdoutToEnd) {
|
||||||
|
hp.push(handler);
|
||||||
|
s.pause();
|
||||||
|
} else {
|
||||||
|
s.push(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const bytesToPush = Math.min(stdoutBuffer.length, n);
|
||||||
|
const bufferToPush = stdoutBuffer.subarray(0, bytesToPush);
|
||||||
|
stdoutBuffer = stdoutBuffer.subarray(bytesToPush);
|
||||||
|
s.push(bufferToPush);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (n != 0) handler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
emulatedStdout.pause(); // Reduce backpressure
|
||||||
|
|
||||||
|
const emulatedStderr = new stream.Readable({
|
||||||
|
read: function () {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
// Begin the request
|
||||||
|
const beginPacket = Buffer.alloc(8);
|
||||||
|
beginPacket.writeUInt16BE(constants.RESPONDER, 0); // FastCGI responder
|
||||||
|
beginPacket.writeUInt8(0, 2); // Don't keep alive
|
||||||
|
writeFastCGIPacket(socket, constants.BEGIN_REQUEST, requestID, beginPacket);
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
let envPacket = Buffer.alloc(0);
|
||||||
|
Object.keys(options.env).forEach((key) => {
|
||||||
|
envPacket = Buffer.concat([
|
||||||
|
envPacket,
|
||||||
|
buildNameValuePair(key, options.env[key])
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
if (envPacket.length > 0)
|
||||||
|
writeFastCGIPacket(socket, constants.PARAMS, requestID, envPacket);
|
||||||
|
writeFastCGIPacket(socket, constants.PARAMS, requestID, Buffer.alloc(0));
|
||||||
|
};
|
||||||
|
|
||||||
|
eventEmitter.stdin = emulatedStdin;
|
||||||
|
eventEmitter.stdout = emulatedStdout;
|
||||||
|
eventEmitter.stderr = emulatedStderr;
|
||||||
|
eventEmitter.init = init;
|
||||||
|
|
||||||
|
// Create socket
|
||||||
|
const socket = net
|
||||||
|
.createConnection(options, () => {
|
||||||
|
eventEmitter.emit("connect");
|
||||||
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
|
stdoutPush(null);
|
||||||
|
emulatedStderr.push(null);
|
||||||
|
eventEmitter.removeAllListeners("exit");
|
||||||
|
eventEmitter.emit("error", err);
|
||||||
|
})
|
||||||
|
.on("data", fastCGISocketHandler);
|
||||||
|
|
||||||
|
eventEmitter.socket = socket;
|
||||||
|
|
||||||
|
eventEmitter.requestID = requestID;
|
||||||
|
|
||||||
|
return eventEmitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
FastCGIHandler: FastCGIHandler,
|
||||||
|
constants: constants,
|
||||||
|
buildFastCGIPacket: buildFastCGIPacket,
|
||||||
|
buildNameValuePair: buildNameValuePair,
|
||||||
|
writeFastCGIPacket: writeFastCGIPacket,
|
||||||
|
parseFastCGIPacket: parseFastCGIPacket
|
||||||
|
};
|
130
tests/utils/cgiParser.test.js
Normal file
130
tests/utils/cgiParser.test.js
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
const createCGIParser = require("../../src/utils/cgiParser.js");
|
||||||
|
|
||||||
|
jest.mock("http"); // Mocking HTTP to simulate the response behavior
|
||||||
|
|
||||||
|
describe("createCGIParser", () => {
|
||||||
|
let req, res, handler, defaultHeaders, software, dataHandler;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
req = {};
|
||||||
|
res = {
|
||||||
|
writeHead: jest.fn(),
|
||||||
|
write: jest.fn(),
|
||||||
|
finished: false,
|
||||||
|
error: jest.fn()
|
||||||
|
};
|
||||||
|
handler = {
|
||||||
|
stdout: {
|
||||||
|
removeAllListeners: jest.fn(),
|
||||||
|
pipe: jest.fn(),
|
||||||
|
on: jest.fn()
|
||||||
|
},
|
||||||
|
removeAllListeners: jest.fn()
|
||||||
|
};
|
||||||
|
defaultHeaders = { "X-Test-Header": "TestValue" };
|
||||||
|
software = "TestSoftware";
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should parse headers and write response correctly", () => {
|
||||||
|
const inputHeaders =
|
||||||
|
"Status: 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, world!";
|
||||||
|
const buffer = Buffer.from(inputHeaders, "latin1");
|
||||||
|
|
||||||
|
dataHandler = createCGIParser(req, res, handler, defaultHeaders, software);
|
||||||
|
dataHandler(buffer);
|
||||||
|
|
||||||
|
expect(res.writeHead).toHaveBeenCalledWith(200, "OK", {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
"Content-Length": "13",
|
||||||
|
"X-Test-Header": "TestValue"
|
||||||
|
});
|
||||||
|
expect(res.write).toHaveBeenCalledWith(
|
||||||
|
Buffer.from("Hello, world!", "latin1")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle status headers without protocol", () => {
|
||||||
|
const inputHeaders =
|
||||||
|
"Status: 200 OK\r\nContent-Type: text/html\r\n\r\n<body>Success</body>";
|
||||||
|
const buffer = Buffer.from(inputHeaders, "latin1");
|
||||||
|
|
||||||
|
dataHandler = createCGIParser(req, res, handler, defaultHeaders, software);
|
||||||
|
dataHandler(buffer);
|
||||||
|
|
||||||
|
expect(res.writeHead).toHaveBeenCalledWith(200, "OK", {
|
||||||
|
"Content-Type": "text/html",
|
||||||
|
"X-Test-Header": "TestValue"
|
||||||
|
});
|
||||||
|
expect(res.write).toHaveBeenCalledWith(
|
||||||
|
Buffer.from("<body>Success</body>", "latin1")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle malformed status line", () => {
|
||||||
|
const inputHeaders =
|
||||||
|
"Content-Type: text/plain\r\n\r\nMalformed status line.";
|
||||||
|
const buffer = Buffer.from(inputHeaders, "latin1");
|
||||||
|
|
||||||
|
dataHandler = createCGIParser(req, res, handler, defaultHeaders, software);
|
||||||
|
dataHandler(buffer);
|
||||||
|
|
||||||
|
expect(res.writeHead).toHaveBeenCalledWith(200, "OK", {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
"X-Test-Header": "TestValue"
|
||||||
|
});
|
||||||
|
expect(res.write).toHaveBeenCalledWith(
|
||||||
|
Buffer.from("Malformed status line.", "latin1")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle redirection logic for Location header", () => {
|
||||||
|
const inputHeaders = "Status: 302 Found\r\nLocation: /new-location\r\n\r\n";
|
||||||
|
const buffer = Buffer.from(inputHeaders, "latin1");
|
||||||
|
|
||||||
|
dataHandler = createCGIParser(req, res, handler, defaultHeaders, software);
|
||||||
|
dataHandler(buffer);
|
||||||
|
|
||||||
|
expect(res.writeHead).toHaveBeenCalledWith(302, "Found", {
|
||||||
|
Location: "/new-location",
|
||||||
|
"X-Test-Header": "TestValue"
|
||||||
|
});
|
||||||
|
expect(res.write).toHaveBeenCalledWith(Buffer.from("", "latin1"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle Set-Cookie headers correctly", () => {
|
||||||
|
const inputHeaders =
|
||||||
|
"Status: 200 OK\r\nSet-Cookie: sessionId=abc123; HttpOnly\r\nSet-Cookie: theme=dark; Secure\r\n\r\nBody content";
|
||||||
|
const buffer = Buffer.from(inputHeaders, "latin1");
|
||||||
|
|
||||||
|
dataHandler = createCGIParser(req, res, handler, defaultHeaders, software);
|
||||||
|
dataHandler(buffer);
|
||||||
|
|
||||||
|
expect(res.writeHead).toHaveBeenCalledWith(200, "OK", {
|
||||||
|
"Set-Cookie": ["sessionId=abc123; HttpOnly", "theme=dark; Secure"],
|
||||||
|
"X-Test-Header": "TestValue"
|
||||||
|
});
|
||||||
|
expect(res.write).toHaveBeenCalledWith(
|
||||||
|
Buffer.from("Body content", "latin1")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should call res.error on failure to writeHead or write", () => {
|
||||||
|
res.writeHead.mockImplementation(() => {
|
||||||
|
throw new Error("writeHead error");
|
||||||
|
});
|
||||||
|
const inputHeaders =
|
||||||
|
"Status: 500 Internal Server Error\r\n\r\nError occurred.";
|
||||||
|
const buffer = Buffer.from(inputHeaders, "latin1");
|
||||||
|
|
||||||
|
dataHandler = createCGIParser(req, res, handler, defaultHeaders, software);
|
||||||
|
dataHandler(buffer);
|
||||||
|
|
||||||
|
expect(res.error).toHaveBeenCalledWith(
|
||||||
|
500,
|
||||||
|
"TestSoftware",
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
expect(handler.removeAllListeners).toHaveBeenCalled();
|
||||||
|
expect(handler.stdout.removeAllListeners).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
150
tests/utils/fastCgi.test.js
Normal file
150
tests/utils/fastCgi.test.js
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
const {
|
||||||
|
FastCGIHandler,
|
||||||
|
constants,
|
||||||
|
buildFastCGIPacket,
|
||||||
|
buildNameValuePair,
|
||||||
|
writeFastCGIPacket
|
||||||
|
} = require("../../src/utils/fastCgi.js");
|
||||||
|
const net = require("net");
|
||||||
|
const EventEmitter = require("events");
|
||||||
|
|
||||||
|
jest.mock("net");
|
||||||
|
|
||||||
|
describe("FastCGI module", () => {
|
||||||
|
let mockSocket;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockSocket = {
|
||||||
|
write: jest.fn(),
|
||||||
|
on: jest.fn().mockImplementation((event, handler) => {
|
||||||
|
if (event === "data") {
|
||||||
|
mockSocket.dataHandler = handler;
|
||||||
|
}
|
||||||
|
return mockSocket; // Enable method chaining
|
||||||
|
}),
|
||||||
|
removeListener: jest.fn().mockImplementation((event, handler) => {
|
||||||
|
if (event === "data" && mockSocket.dataHandler == handler) {
|
||||||
|
mockSocket.dataHandler = () => {};
|
||||||
|
}
|
||||||
|
return mockSocket; // Enable method chaining
|
||||||
|
}),
|
||||||
|
dataHandler: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
net.createConnection.mockReturnValue(mockSocket);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should create a socket and connect", () => {
|
||||||
|
const handler = new FastCGIHandler({ host: "localhost", port: 9000 });
|
||||||
|
|
||||||
|
expect(net.createConnection).toHaveBeenCalledWith(
|
||||||
|
{ host: "localhost", port: 9000 },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockSocket.on).toHaveBeenCalledWith("error", expect.any(Function));
|
||||||
|
expect(mockSocket.on).toHaveBeenCalledWith("data", expect.any(Function));
|
||||||
|
expect(handler).toBeInstanceOf(EventEmitter);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should build a FastCGI packet", () => {
|
||||||
|
const content = Buffer.from("test");
|
||||||
|
|
||||||
|
const packet = buildFastCGIPacket(1, 1, content);
|
||||||
|
expect(packet.length).toBe(16); // Header + padded content
|
||||||
|
expect(packet.readUInt8(0)).toBe(1); // version
|
||||||
|
expect(packet.readUInt8(1)).toBe(1); // type
|
||||||
|
expect(packet.readUInt16BE(2)).toBe(1); // requestID
|
||||||
|
expect(packet.readUInt16BE(4)).toBe(4); // content length
|
||||||
|
expect(packet.readUInt8(6)).toBe(4); // padding length
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should write FastCGI packets to the socket", () => {
|
||||||
|
const handler = new FastCGIHandler({});
|
||||||
|
const content = Buffer.from("test");
|
||||||
|
|
||||||
|
writeFastCGIPacket(handler.socket, 1, handler.requestID, content);
|
||||||
|
expect(mockSocket.write).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockSocket.write).toHaveBeenCalledWith(expect.any(Buffer));
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line jest/no-done-callback
|
||||||
|
test("should handle socket data and emit events", (done) => {
|
||||||
|
const handler = new FastCGIHandler({});
|
||||||
|
handler.stdout.on("data", (chunk) => {
|
||||||
|
expect(chunk).toStrictEqual(Buffer.from("Hello World"));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate a FastCGI STDOUT packet
|
||||||
|
const packet = buildFastCGIPacket(
|
||||||
|
constants.STDOUT,
|
||||||
|
handler.requestID,
|
||||||
|
Buffer.from("Hello World")
|
||||||
|
);
|
||||||
|
|
||||||
|
mockSocket.dataHandler(packet);
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line jest/no-done-callback
|
||||||
|
test("should handle END_REQUEST and emit 'exit' event", (done) => {
|
||||||
|
const handler = new FastCGIHandler({});
|
||||||
|
|
||||||
|
handler.on("exit", (code, signal) => {
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(signal).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate an END_REQUEST packet
|
||||||
|
const content = Buffer.alloc(8);
|
||||||
|
content.writeUInt32BE(0, 0); // appStatus
|
||||||
|
content.writeUInt8(0, 4); // protocolStatus
|
||||||
|
const packet = buildFastCGIPacket(
|
||||||
|
constants.END_REQUEST,
|
||||||
|
handler.requestID,
|
||||||
|
content
|
||||||
|
);
|
||||||
|
|
||||||
|
mockSocket.dataHandler(packet);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle UNKNOWN_TYPE and discard it", () => {
|
||||||
|
const handler = new FastCGIHandler({});
|
||||||
|
const dataListener = jest.fn();
|
||||||
|
handler.stdout.on("data", dataListener);
|
||||||
|
|
||||||
|
// Simulate an UNKNOWN_TYPE packet
|
||||||
|
const packet = buildFastCGIPacket(
|
||||||
|
constants.UNKNOWN_TYPE,
|
||||||
|
handler.requestID,
|
||||||
|
Buffer.alloc(0)
|
||||||
|
);
|
||||||
|
|
||||||
|
mockSocket.dataHandler(packet);
|
||||||
|
expect(dataListener).not.toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should build name-value pairs correctly", () => {
|
||||||
|
const pair = buildNameValuePair("key", "value");
|
||||||
|
expect(pair.length).toBe(10); // 1 byte for key length, 1 for value length, and key-value content
|
||||||
|
expect(pair.toString("utf8", 2)).toBe("keyvalue");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should send environment variables on init", () => {
|
||||||
|
const env = { FOO: "BAR", BAZ: "QUX" };
|
||||||
|
const handler = new FastCGIHandler({ env });
|
||||||
|
|
||||||
|
handler.init();
|
||||||
|
|
||||||
|
expect(mockSocket.write).toHaveBeenCalled();
|
||||||
|
const calls = mockSocket.write.mock.calls;
|
||||||
|
|
||||||
|
const envPacket = calls.find(([arg]) => arg.includes(Buffer.from("FOO")));
|
||||||
|
expect(envPacket).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue