chore: init
Some checks failed
Repo sync GitHub -> SVR.JS Git server / svrjsgit (push) Has been cancelled
Some checks failed
Repo sync GitHub -> SVR.JS Git server / svrjsgit (push) Has been cancelled
This commit is contained in:
commit
67c0c8b97f
19 changed files with 9204 additions and 0 deletions
19
.github/workflows/main.yml
vendored
Normal file
19
.github/workflows/main.yml
vendored
Normal 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
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.
|
24
README
Normal file
24
README
Normal 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
3
commitlint.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ["@commitlint/config-conventional"]
|
||||
};
|
40
esbuild.config.js
Normal file
40
esbuild.config.js
Normal 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
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
modInfo.json
Normal file
4
modInfo.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Integration with ModSecurity",
|
||||
"version": "Nightly-GitMain"
|
||||
}
|
6
nodemon.json
Normal file
6
nodemon.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"watch": [
|
||||
"svrjs/mods/mod.js",
|
||||
"svrjs/config.json"
|
||||
]
|
||||
}
|
8081
package-lock.json
generated
Normal file
8081
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
46
package.json
Normal file
46
package.json
Normal 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
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;
|
534
src/index.js
Normal file
534
src/index.js
Normal 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
122
svrjs.install.js
Normal 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
235
tests/index.test.js
Normal 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}`
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue