chore: init
Some checks failed
Repo sync GitHub -> SVR.JS Git server / svrjsgit (push) Has been cancelled

This commit is contained in:
Dorian Niemiec 2025-01-12 18:33:26 +01:00
commit 67c0c8b97f
19 changed files with 9204 additions and 0 deletions

19
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,19 @@
# Sync repo to the Codeberg mirror
name: Repo sync GitHub -> SVR.JS Git server
on:
push:
branches:
- '**'
jobs:
svrjsgit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: spyoungtech/mirror-action@v0.5.1
with:
REMOTE: "https://git.svrjs.org/svrjs/svrjs-mod-starter.git"
GIT_USERNAME: github-mirror
GIT_PASSWORD: ${{ secrets.GIT_PASSWORD }}

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
# Build output
/dist/
# SVR.JS
/svrjs/
# Dependencies
node_modules
# ESLint cache
.eslintcache

2
.husky/commit-msg Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
npx --no -- commitlint --edit "$1"

2
.husky/pre-commit Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
npx lint-staged

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018-2024 SVR.JS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

24
README Normal file
View file

@ -0,0 +1,24 @@
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 start the development server for the SVR.JS mod, run "npm run dev".
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".
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
- nodemon.json - Nodemon configuration
- prettier.config.js - Prettier configuration
- svrjs.install.js - SVR.JS installation script (SVR.JS is used by the development server)

3
commitlint.config.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
extends: ["@commitlint/config-conventional"]
};

40
esbuild.config.js Normal file
View file

@ -0,0 +1,40 @@
const esbuild = require("esbuild");
const isDev = process.env.NODE_ENV == "development";
if (!isDev) {
esbuild
.build({
entryPoints: ["src/index.js"],
bundle: true,
outfile: "dist/mod.js", // Mod output file
platform: "node",
target: "es2017",
external: ["modsecurity"]
})
.catch((err) => {
throw err;
});
} else {
esbuild
.context({
entryPoints: ["src/index.js"],
bundle: true,
outfile: "svrjs/mods/mod.js", // Mod output file
platform: "node",
target: "es2017",
external: ["modsecurity"]
})
.then((ctx) => {
ctx
.watch()
.then(() => {
console.log("Watching for changes in SVR.JS mod source code...");
})
.catch((err) => {
throw err;
});
})
.catch((err) => {
throw err;
});
}

30
eslint.config.js Normal file
View 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
View file

@ -0,0 +1,5 @@
module.exports = {
testEnvironment: 'node',
testMatch: ['**/tests/**/*.test.js'],
verbose: true,
};

4
lint-staged.config.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = {
"tests/**/*.js": "eslint --cache --fix",
"src/**/*.js": "eslint --cache --fix"
};

4
modInfo.json Normal file
View file

@ -0,0 +1,4 @@
{
"name": "Integration with ModSecurity",
"version": "Nightly-GitMain"
}

6
nodemon.json Normal file
View file

@ -0,0 +1,6 @@
{
"watch": [
"svrjs/mods/mod.js",
"svrjs/config.json"
]
}

8081
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

46
package.json Normal file
View file

@ -0,0 +1,46 @@
{
"name": "svrjs-mod-starter",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "npm run clean && node esbuild.config.js",
"clean": "rimraf dist",
"cz": "cz",
"dev": "npm run clean && concurrently \"NODE_ENV=development node esbuild.config.js\" \"wait-on svrjs/mods/mod.js && nodemon svrjs/svr.js --stdout-notty --no-save-config\"",
"lint": "eslint --no-error-on-unmatched-pattern src/**/*.js src/*.js tests/**/*.test.js tests/**/*.js",
"lint:fix": "npm run lint -- --fix",
"postinstall": "node svrjs.install.js",
"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",
"concurrently": "^9.1.2",
"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.3.0",
"nodemon": "^3.1.9",
"prettier": "^3.3.3",
"rimraf": "^5.0.10",
"wait-on": "^8.0.1",
"zip": "^1.2.0"
},
"dependencies": {
"modsecurity": "^0.0.2"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}

15
prettier.config.js Normal file
View 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;

534
src/index.js Normal file
View file

@ -0,0 +1,534 @@
const path = require("path");
const { ModSecurity, Rules, Transaction } = require("modsecurity");
const modInfo = require("../modInfo.json"); // SVR.JS mod information
const modSecurity = new ModSecurity();
const rules = new Rules();
let rulesError = null;
try {
if (!process.serverConfig.modSecurityRulesPath)
throw new Error(
'You need to specify the path to ModSecurity rules file in the "modSecurityRulesPath" SVR.JS configuration property.'
);
rules.loadFromFile(
path.isAbsolute(process.serverConfig.modSecurityRulesPath)
? process.serverConfig.modSecurityRulesPath
: process.dirname + "/" + process.serverConfig.modSecurityRulesPath
);
} catch (err) {
rulesError = err;
}
// Exported SVR.JS mod callback
module.exports = (req, res, logFacilities, config, next) => {
if (rulesError) {
res.error(500, `modsecurity-integration/${modInfo.version}`, rulesError);
}
let requestBody = Buffer.alloc(0);
const maxRequestCheckedSize =
config.maxRequestCheckedSize !== undefined
? config.maxRequestCheckedSize
: 65536;
const maxRequestCheckedSizeStrict =
config.maxRequestCheckedSizeStrict !== undefined
? config.maxRequestCheckedSizeStrict
: false;
const maxResponseCheckedSize =
config.maxResponseCheckedSize !== undefined
? config.maxRequestCheckedSize
: 65536;
const modSecurityCallback = () => {
const transaction = new Transaction(modSecurity, rules);
let securityResponse;
// Capture the response
const originalSetHeader = res.setHeader.bind(res);
const originalRemoveHeader = res.removeHeader.bind(res);
const originalWriteHead = res.writeHead.bind(res);
const originalWrite = res.write.bind(res);
const originalEnd = res.end.bind(res);
let writtenHeaders = res.getHeaders();
let writtenStatusCode = 200;
let writtenStatusCodeDescription = null;
let headersSet = false;
let responseBuffer = Buffer.alloc(0);
let maxResponseCheckedSizeExceeded = maxResponseCheckedSize <= 0;
const responseWriteCallbacks = [];
res.setHeader = function (name, value) {
writtenHeaders[name.toLowerCase()] = value;
return originalSetHeader(name, value);
};
res.removeHeader = function (name) {
delete writtenHeaders[name.toLowerCase()];
return originalRemoveHeader(name);
};
res.writeHead = function (statusCode, statusCodeDescription, headers) {
const properHeaders = headers ? headers : statusCodeDescription;
if (typeof properHeaders === "object" && properHeaders !== null) {
Object.keys(properHeaders).forEach((key) => {
writtenHeaders[key.toLowerCase()] = properHeaders[key];
});
}
writtenStatusCode = statusCode;
let intervene = false;
Object.keys(writtenHeaders).forEach((key) => {
// There is a typo in "modsecurity" npm package C++ code that causes the value to be a header name when the value is not a buffer.
if (typeof writtenHeaders[key] == "string") {
securityResponse = transaction.addResponseHeader(
Buffer.from(key),
Buffer.from(writtenHeaders[key])
);
if (typeof securityResponse === "object") {
intervene = true;
return;
}
} else if (Array.isArray(writtenHeaders[key])) {
writtenHeaders[key].forEach((value) => {
securityResponse = transaction.addResponseHeader(
Buffer.from(key),
Buffer.from(value)
);
if (typeof securityResponse === "object") {
intervene = true;
return;
}
});
if (intervene) return;
}
});
if (intervene) {
res.write = () => false;
res.end = () => {
processIntervention();
return false;
};
return res;
} else {
securityResponse = transaction.processResponseHeaders(
writtenStatusCode,
req.httpVersion
);
if (typeof securityResponse === "object") {
res.write = () => false;
res.end = () => {
processIntervention();
return false;
};
return res;
} else {
headersSet = true;
if (!maxResponseCheckedSizeExceeded) {
if (headers || typeof statusCodeDescription !== "object") {
writtenStatusCodeDescription = statusCodeDescription;
}
} else {
if (headers || typeof statusCodeDescription !== "object") {
originalWriteHead(
writtenStatusCode,
statusCodeDescription,
writtenHeaders
);
} else {
originalWriteHead(writtenStatusCode, writtenHeaders);
}
}
return res;
}
}
};
res.end = function (chunk, encoding, callback) {
if (!headersSet) {
let intervene = false;
Object.keys(writtenHeaders).forEach((key) => {
// There is a typo in "modsecurity" npm package C++ code that causes the value to be a header name when the value is not a buffer.
if (typeof writtenHeaders[key] == "string") {
securityResponse = transaction.addResponseHeader(
Buffer.from(key),
Buffer.from(writtenHeaders[key])
);
if (typeof securityResponse === "object") {
intervene = true;
return;
}
} else if (Array.isArray(writtenHeaders[key])) {
writtenHeaders[key].forEach((value) => {
securityResponse = transaction.addResponseHeader(
Buffer.from(key),
Buffer.from(value)
);
if (typeof securityResponse === "object") {
intervene = true;
return;
}
});
if (intervene) return;
}
});
if (intervene) {
return false;
} else {
writtenStatusCode = res.statusCode;
securityResponse = transaction.processResponseHeaders(
writtenStatusCode,
req.httpVersion
);
if (typeof securityResponse === "object") {
return false;
}
headersSet = true;
}
}
const processedChunk = chunk
? Buffer.from(
chunk,
typeof encoding === "string" ? encoding : undefined
)
: Buffer.alloc(0);
if (
!maxResponseCheckedSizeExceeded &&
responseBuffer.length + processedChunk.length > maxResponseCheckedSize
) {
maxResponseCheckedSizeExceeded = true;
if (headersSet) {
if (writtenStatusCodeDescription) {
originalWriteHead(
writtenStatusCode,
writtenStatusCodeDescription,
writtenHeaders
);
} else {
originalWriteHead(writtenStatusCode, writtenHeaders);
}
}
originalWrite(responseBuffer);
responseWriteCallbacks.forEach((callback) => callback());
originalWrite(chunk, encoding, callback);
} else if (!maxResponseCheckedSizeExceeded) {
securityResponse = transaction.appendResponseBody(responseBuffer);
if (typeof securityResponse === "object") {
res.write = () => false;
res.end = () => {
processIntervention();
return false;
};
return false;
}
if (req.method != "HEAD" && chunk) {
securityResponse = transaction.appendResponseBody(
Buffer.from(
chunk,
typeof encoding === "string" ? encoding : undefined
)
);
if (typeof securityResponse === "object") {
processIntervention();
return false;
}
}
}
securityResponse = transaction.processResponseBody();
if (typeof securityResponse === "object") {
processIntervention();
return false;
}
transaction.processLogging();
if (!maxResponseCheckedSizeExceeded) {
if (headersSet) {
if (writtenStatusCodeDescription) {
originalWriteHead(
writtenStatusCode,
writtenStatusCodeDescription,
writtenHeaders
);
} else {
originalWriteHead(writtenStatusCode, writtenHeaders);
}
}
originalWrite(responseBuffer);
responseWriteCallbacks.forEach((callback) => callback());
}
const endResult = originalEnd(chunk, encoding, callback);
return endResult;
};
if (req.method != "HEAD") {
res.write = function (chunk, encoding, callback) {
if (!headersSet) {
let intervene = false;
Object.keys(writtenHeaders).forEach((key) => {
// There is a typo in "modsecurity" npm package C++ code that causes the value to be a header name when the value is not a buffer.
if (typeof writtenHeaders[key] == "string") {
securityResponse = transaction.addResponseHeader(
Buffer.from(key),
Buffer.from(writtenHeaders[key])
);
if (typeof securityResponse === "object") {
intervene = true;
return;
}
} else if (Array.isArray(writtenHeaders[key])) {
writtenHeaders[key].forEach((value) => {
securityResponse = transaction.addResponseHeader(
Buffer.from(key),
Buffer.from(value)
);
if (typeof securityResponse === "object") {
intervene = true;
return;
}
});
if (intervene) return;
}
});
if (intervene) {
return false;
} else {
writtenStatusCode = res.statusCode;
securityResponse = transaction.processResponseHeaders(
writtenStatusCode,
req.httpVersion
);
if (typeof securityResponse === "object") {
return false;
}
headersSet = true;
}
}
const processedChunk = chunk
? Buffer.from(
chunk,
typeof encoding === "string" ? encoding : undefined
)
: Buffer.alloc(0);
if (maxResponseCheckedSizeExceeded) {
return originalWrite(chunk, encoding, callback);
} else if (
responseBuffer.length + processedChunk.length >
maxResponseCheckedSize
) {
maxResponseCheckedSizeExceeded = true;
if (headersSet) {
if (writtenStatusCodeDescription) {
originalWriteHead(
writtenStatusCode,
writtenStatusCodeDescription,
writtenHeaders
);
} else {
originalWriteHead(writtenStatusCode, writtenHeaders);
}
}
originalWrite(responseBuffer);
responseWriteCallbacks.forEach((callback) => callback());
return originalWrite(chunk, encoding, callback);
} else {
responseBuffer = Buffer.concat([responseBuffer, processedChunk]);
if (typeof callback === "function")
responseWriteCallbacks.push(callback);
return true;
}
};
}
const restoreResponseMethods = () => {
// Restore response methods
res.setHeader = originalSetHeader;
res.removeHeader = originalRemoveHeader;
res.writeHead = originalWriteHead;
res.write = originalWrite;
res.end = originalEnd;
};
const processIntervention = () => {
restoreResponseMethods();
const reason = securityResponse.log
? securityResponse.log
: "No reason provided by ModSecurity.";
res.error(
securityResponse.status,
`modsecurity-integration/${modInfo.version}`,
new Error(reason)
);
logFacilities.errmessage("Request blocked by ModSecurity.");
logFacilities.errmessage(reason);
// Update the HTTP status code just for ModSecurity audit log
if (headersSet) {
transaction.updateStatusCode(securityResponse.status);
} else {
transaction.processResponseHeaders(
securityResponse.status,
req.httpVersion
);
}
transaction.processLogging();
};
const reqip = req.socket.realRemoteAddress
? req.socket.realRemoteAddress
: req.socket.remoteAddress;
const reqport = req.socket.realRemotePort
? req.socket.realRemotePort
: req.socket.remotePort;
securityResponse = transaction.processConnection(
reqip,
reqport,
req.socket.localAddress,
req.socket.localPort
);
if (typeof securityResponse === "object") {
return processIntervention();
}
if (false === securityResponse) {
restoreResponseMethods();
res.error(
500,
`modsecurity-integration/${modInfo.version}`,
new Error("Unknown ModSecurity error.")
);
return;
}
securityResponse = transaction.processURI(
req.url,
req.method,
req.httpVersion
);
if (typeof securityResponse === "object") {
return processIntervention();
}
let key = null;
req.rawHeaders.forEach((v) => {
if (key === null) {
key = v;
} else {
// There is a typo in "modsecurity" npm package C++ code that causes the value to be a header name when the value is not a buffer.
transaction.addRequestHeader(Buffer.from(key), Buffer.from(v));
key = null;
}
});
securityResponse = transaction.processRequestHeaders();
if (typeof securityResponse === "object") {
return processIntervention();
}
if (requestBody && requestBody.length > 0) {
securityResponse = transaction.appendRequestBody(requestBody);
if (typeof securityResponse === "object") {
return processIntervention();
}
}
securityResponse = transaction.processRequestBody();
if (typeof securityResponse === "object") {
return processIntervention();
}
next();
};
//req.body
function readableHandlerPredict() {
try {
if (req._readableState.buffer.head !== null) {
requestBody = Buffer.from(req._readableState.buffer.head.data);
}
// eslint-disable-next-line no-unused-vars
} catch (err) {
// Request body setting failed
}
if (!requestBody) requestBody = Buffer.alloc(0);
modSecurityCallback();
}
let chunkBuffer = [];
let chunkBufferLength = Math.min(
parseInt(req.headers["content-length"]) - 1,
maxRequestCheckedSize
);
let chunkBufferPointer = 0;
const readableHandlerWhole = () => {
let chunk = req.read(chunkBufferLength - chunkBufferPointer);
if (!chunk) chunk = req.read();
if (chunk) {
const chunk2 = Buffer.from(chunk);
chunkBuffer.push(chunk2);
chunkBufferPointer += chunk2.length;
}
if (req._readableState.ended) {
req.removeListener("readable", readableHandlerWhole);
res.error(400, `modsecurity-integration/${modInfo.version}`);
} else if (chunkBufferPointer >= chunkBufferLength) {
req.removeListener("readable", readableHandlerWhole);
try {
for (let i = chunkBuffer.length - 1; i >= 0; i--) {
req.unshift(chunkBuffer[i]);
}
} catch (err) {
res.error(500, `modsecurity-integration/${modInfo.version}`, err);
return;
}
requestBody = Buffer.concat([requestBody, ...chunkBuffer]);
modSecurityCallback();
}
};
if (
!req._readableState.ended &&
!isNaN(chunkBufferLength) &&
chunkBufferLength > 0
) {
if (
maxRequestCheckedSizeStrict &&
parseInt(req.headers["content-length"]) > maxRequestCheckedSize
) {
res.error(413, `modsecurity-integration/${modInfo.version}`);
}
req.on("readable", readableHandlerWhole);
} else if (req._readableState.length > 0 || req._readableState.ended) {
readableHandlerPredict();
} else {
req.once("readable", readableHandlerPredict);
}
};
module.exports.configValidators = {
modSecurityRulesPath: (value) => typeof value === "string",
maxRequestCheckedSize: (value) => typeof value === "number" && value >= 0,
maxRequestCheckedSizeStrict: (value) => typeof value === "boolean",
maxResponseCheckedSize: (value) => typeof value === "number" && value >= 0
};
module.exports.modInfo = modInfo;

122
svrjs.install.js Normal file
View file

@ -0,0 +1,122 @@
// Code in the SVR.JS installation script is derived from the "create-svrjs-server" command.
const fs = require("fs");
const https = require("https");
const zip = require("zip");
const zlib = require("zlib");
function downloadSVRJS(version) {
const normalizedVersion = version.toLowerCase().replace(/[^0-9a-z]+/g, ".");
let path = "/svr.js." + normalizedVersion + ".zip";
if (normalizedVersion.indexOf("beta") != -1)
path = "/beta/svr.js." + normalizedVersion + ".zip";
if (normalizedVersion.indexOf("nightly") != -1)
path = "/nightly/svr.js." + normalizedVersion + ".zip";
https
.get(
{
hostname: "downloads.svrjs.org",
port: 443,
path: path,
method: "GET"
},
(res) => {
let zipData = "";
if (res.statusCode != 200) {
console.log(
"Server returned the " +
res.statusCode +
" HTTP status code while trying to download SVR.JS " +
version
);
process.exit(1);
}
res.on("data", (chunk) => {
zipData += chunk.toString("latin1");
});
res.on("end", function () {
console.log("Downloaded the .zip file");
zipData = Buffer.from(zipData, "latin1");
const reader = zip.Reader(zipData);
const allFiles = reader.toObject();
Object.keys(allFiles).forEach((filename) => {
const paths = filename.split("/");
for (let i = 0; i < paths.length - 1; i++) {
const dirname = JSON.parse(JSON.stringify(paths))
.splice(0, i + 1)
.join("/");
if (!fs.existsSync(__dirname + "/svrjs/" + dirname)) {
fs.mkdirSync(__dirname + "/svrjs/" + dirname);
}
}
fs.writeFileSync(
__dirname + "/svrjs/" + filename,
allFiles[filename]
);
});
if (!fs.existsSync(__dirname + "/svrjs/mods")) fs.mkdirSync(__dirname + "/svrjs/mods");
if (fs.existsSync(__dirname + "/svrjs/svr.compressed")) {
console.log("Deleting SVR.JS stub...");
fs.unlinkSync(__dirname + "/svrjs/svr.js");
// Modules aren't extracted in SVR.JS's 4.x stub, so no module extraction code here.
console.log("Decompressing SVR.JS...");
const script = zlib.gunzipSync(
fs.readFileSync(__dirname + "/svrjs/svr.compressed")
);
fs.unlinkSync(__dirname + "/svrjs/svr.compressed");
fs.writeFileSync(__dirname + "/svrjs/svr.js", script);
}
console.log("SVR.JS is installed successfully.");
});
}
)
.on("error", function () {
console.log("Can't connect to the SVR.JS download server!");
process.exit(1);
});
}
if (fs.existsSync(__dirname + "/svrjs")) {
console.log("SVR.JS is already installed.");
} else {
fs.mkdirSync(__dirname + "/svrjs");
https
.get(
{
hostname: "downloads.svrjs.org",
port: 443,
path: "/latest.svrjs",
method: "GET"
},
(res) => {
if (res.statusCode != 200) {
console.log(
"Server returned the " + res.statusCode + " HTTP status code"
);
process.exit(1);
}
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
const selectedVersion = data.trim();
if (!selectedVersion) {
console.log(
"Can't obtain the latest version from downloads server"
);
process.exit(1);
} else {
console.log("Selected SVR.JS " + selectedVersion);
downloadSVRJS(selectedVersion);
}
});
}
)
.on("error", function () {
console.log("Can't connect to the SVR.JS download server!");
process.exit(1);
});
}

235
tests/index.test.js Normal file
View file

@ -0,0 +1,235 @@
const { EventEmitter } = require("events");
const { Transaction } = require("modsecurity");
jest.mock("modsecurity");
process.serverConfig = {
modSecurityRulesPath: "/path/to/rules"
};
const modSecurityIntegration = require("../src/index.js");
const modInfo = require("../modInfo.json");
describe("ModSecurity Integration", () => {
let req, res, logFacilities, config, next;
beforeEach(() => {
req = new EventEmitter();
req.read = jest.fn();
req.unshift = jest.fn();
req.method = "GET";
req.url = "/test";
req.httpVersion = "1.1";
req.rawHeaders = ["Host", "example.com"];
req.socket = {
realRemoteAddress: "127.0.0.1",
realRemotePort: 12345,
localAddress: "127.0.0.1",
localPort: 8080
};
req.headers = {};
req._readableState = {
length: 0,
ended: true
};
res = new EventEmitter();
res.setHeader = jest.fn();
res.removeHeader = jest.fn();
res.writeHead = jest.fn();
res.write = jest.fn();
res.end = jest.fn();
res.error = jest.fn();
res.getHeaders = jest.fn().mockReturnValue({});
logFacilities = {
errmessage: jest.fn()
};
config = {};
next = jest.fn();
jest.clearAllMocks();
Transaction.mockReset();
});
test("should call next if no rules error and no security response", () => {
modSecurityIntegration(req, res, logFacilities, config, next);
expect(next).toHaveBeenCalled();
});
test("should process connection and call next if no security response", () => {
Transaction.prototype.processConnection.mockReturnValue(true);
modSecurityIntegration(req, res, logFacilities, config, next);
expect(Transaction.prototype.processConnection).toHaveBeenCalledWith(
"127.0.0.1",
12345,
"127.0.0.1",
8080
);
expect(next).toHaveBeenCalled();
});
test("should process URI and call next if no security response", () => {
Transaction.prototype.processConnection.mockReturnValue(true);
Transaction.prototype.processURI.mockReturnValue(true);
modSecurityIntegration(req, res, logFacilities, config, next);
expect(Transaction.prototype.processURI).toHaveBeenCalledWith(
"/test",
"GET",
"1.1"
);
expect(next).toHaveBeenCalled();
});
test("should process request headers and call next if no security response", () => {
Transaction.prototype.processConnection.mockReturnValue(true);
Transaction.prototype.processURI.mockReturnValue(true);
Transaction.prototype.processRequestHeaders.mockReturnValue(true);
modSecurityIntegration(req, res, logFacilities, config, next);
expect(Transaction.prototype.processRequestHeaders).toHaveBeenCalled();
expect(next).toHaveBeenCalled();
});
test("should handle security response and call processIntervention", () => {
const securityResponse = { status: 403, log: "Blocked by ModSecurity" };
Transaction.prototype.processConnection.mockReturnValue(securityResponse);
modSecurityIntegration(req, res, logFacilities, config, next);
expect(res.error).toHaveBeenCalledWith(
403,
`modsecurity-integration/${modInfo.version}`,
new Error("Blocked by ModSecurity")
);
expect(logFacilities.errmessage).toHaveBeenCalledWith(
"Request blocked by ModSecurity."
);
expect(logFacilities.errmessage).toHaveBeenCalledWith(
"Blocked by ModSecurity"
);
});
test("should handle unknown ModSecurity error", () => {
Transaction.prototype.processConnection.mockReturnValue(false);
modSecurityIntegration(req, res, logFacilities, config, next);
expect(res.error).toHaveBeenCalledWith(
500,
`modsecurity-integration/${modInfo.version}`,
new Error("Unknown ModSecurity error.")
);
});
test("should handle request body and call next if no security response", () => {
Transaction.prototype.processConnection.mockReturnValue(true);
Transaction.prototype.processURI.mockReturnValue(true);
Transaction.prototype.processRequestHeaders.mockReturnValue(true);
Transaction.prototype.appendRequestBody.mockReturnValue(true);
Transaction.prototype.processRequestBody.mockReturnValue(true);
modSecurityIntegration(req, res, logFacilities, config, next);
expect(Transaction.prototype.processRequestBody).toHaveBeenCalled();
expect(next).toHaveBeenCalled();
});
test("should block request if security response is detected during processResponseHeaders", () => {
const securityResponse = { status: 403, log: "Blocked by ModSecurity" };
Transaction.prototype.processConnection.mockReturnValue(true);
Transaction.prototype.processURI.mockReturnValue(true);
Transaction.prototype.processRequestHeaders.mockReturnValue(true);
Transaction.prototype.processResponseHeaders.mockReturnValue(
securityResponse
);
modSecurityIntegration(req, res, logFacilities, config, next);
res.writeHead(200, "OK");
res.write("Something that will trigger security response.");
res.end();
expect(res.error).toHaveBeenCalledWith(
403,
`modsecurity-integration/${modInfo.version}`,
new Error("Blocked by ModSecurity")
);
expect(logFacilities.errmessage).toHaveBeenCalledWith(
"Request blocked by ModSecurity."
);
expect(logFacilities.errmessage).toHaveBeenCalledWith(
"Blocked by ModSecurity"
);
});
test("should block request if security response is detected during appendResponseBody", () => {
const securityResponse = { status: 403, log: "Blocked by ModSecurity" };
Transaction.prototype.processConnection.mockReturnValue(true);
Transaction.prototype.processURI.mockReturnValue(true);
Transaction.prototype.processRequestHeaders.mockReturnValue(true);
Transaction.prototype.appendResponseBody.mockReturnValue(securityResponse);
modSecurityIntegration(req, res, logFacilities, config, next);
res.writeHead(200, "OK");
res.write("Something that will trigger security response.");
res.end();
expect(res.error).toHaveBeenCalledWith(
403,
`modsecurity-integration/${modInfo.version}`,
new Error("Blocked by ModSecurity")
);
expect(logFacilities.errmessage).toHaveBeenCalledWith(
"Request blocked by ModSecurity."
);
expect(logFacilities.errmessage).toHaveBeenCalledWith(
"Blocked by ModSecurity"
);
});
test("should block request if security response is detected during processResponseBody", () => {
const securityResponse = { status: 403, log: "Blocked by ModSecurity" };
Transaction.prototype.processConnection.mockReturnValue(true);
Transaction.prototype.processURI.mockReturnValue(true);
Transaction.prototype.processRequestHeaders.mockReturnValue(true);
Transaction.prototype.processResponseBody.mockReturnValue(securityResponse);
modSecurityIntegration(req, res, logFacilities, config, next);
res.writeHead(200, "OK");
res.write("Something that will trigger security response.");
res.end();
expect(res.error).toHaveBeenCalledWith(
403,
`modsecurity-integration/${modInfo.version}`,
new Error("Blocked by ModSecurity")
);
expect(logFacilities.errmessage).toHaveBeenCalledWith(
"Request blocked by ModSecurity."
);
expect(logFacilities.errmessage).toHaveBeenCalledWith(
"Blocked by ModSecurity"
);
});
test("should handle maxRequestCheckedSize exceeded and call next", () => {
config.maxRequestCheckedSize = 10;
req.headers["content-length"] = 20;
req._readableState.ended = false;
req._readableState.length = 0;
req.read.mockImplementation((size) => Buffer.alloc(Math.min(20, size)));
modSecurityIntegration(req, res, logFacilities, config, next);
req.emit("readable");
expect(next).toHaveBeenCalled();
});
test("should handle maxRequestCheckedSizeStrict", () => {
config.maxRequestCheckedSizeStrict = true;
config.maxRequestCheckedSize = 10;
req.headers["content-length"] = 20;
req._readableState.ended = false;
req._readableState.length = 0;
modSecurityIntegration(req, res, logFacilities, config, next);
expect(res.error).toHaveBeenCalledWith(
413,
`modsecurity-integration/${modInfo.version}`
);
});
});