diff --git a/package-lock.json b/package-lock.json
index 63bdca2..90a165b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,6 +25,7 @@
"eslint-plugin-prettier": "^5.2.1",
"globals": "^15.9.0",
"jest": "^29.7.0",
+ "node-mocks-http": "^1.15.1",
"prettier": "^3.3.3"
}
},
@@ -1753,6 +1754,49 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/body-parser": {
+ "version": "1.19.5",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
+ "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
+ "dev": true,
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
+ "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.19.5",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz",
+ "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -1762,6 +1806,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/http-errors": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
+ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
+ "dev": true
+ },
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -1786,6 +1836,12 @@
"@types/istanbul-lib-report": "*"
}
},
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true
+ },
"node_modules/@types/node": {
"version": "22.5.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz",
@@ -1795,6 +1851,39 @@
"undici-types": "~6.19.2"
}
},
+ "node_modules/@types/qs": {
+ "version": "6.9.15",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
+ "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==",
+ "dev": true
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true
+ },
+ "node_modules/@types/send": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
+ "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
+ "dev": true,
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.7",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
+ "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
+ "dev": true,
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "*"
+ }
+ },
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@@ -1961,6 +2050,19 @@
"node": ">=6.5"
}
},
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dev": true,
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@@ -2659,6 +2761,18 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2793,6 +2907,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -3473,6 +3596,15 @@
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -4905,6 +5037,24 @@
"tmpl": "1.0.5"
}
},
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -4920,6 +5070,15 @@
"node": ">= 8"
}
},
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -4933,6 +5092,18 @@
"node": ">=8.6"
}
},
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -5038,12 +5209,44 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
"dev": true
},
+ "node_modules/node-mocks-http": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.15.1.tgz",
+ "integrity": "sha512-X/GpUpNNiPDYUeUD183W8V4OW6OHYWI29w/QDyb+c/GzOfVEAlo6HjbW9++eXT2aV2lGg+uS+XqTD2q0pNREQA==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "^4.17.21",
+ "@types/node": "*",
+ "accepts": "^1.3.7",
+ "content-disposition": "^0.5.3",
+ "depd": "^1.1.0",
+ "fresh": "^0.5.2",
+ "merge-descriptors": "^1.0.1",
+ "methods": "^1.1.2",
+ "mime": "^1.3.4",
+ "parseurl": "^1.3.3",
+ "range-parser": "^1.2.0",
+ "type-is": "^1.6.18"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
@@ -5214,6 +5417,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5524,6 +5736,15 @@
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
"dev": true
},
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -6214,6 +6435,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dev": true,
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/typescript": {
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
diff --git a/package.json b/package.json
index d6289c4..8bd011e 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,9 @@
"lint": "eslint --no-error-on-unmatched-pattern src/**/*.js src/*.js tests/**/*.test.js tests/**/*.js tests/*.test.js tests/*.js utils/**/*.js utils/*.js",
"lint:fix": "npm run lint -- --fix",
"start": "node dist/svr.js",
- "test": "jest"
+ "test": "jest",
+ "test:middleware": "jest tests/middleware",
+ "test:utils": "jest tests/utils"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
@@ -22,6 +24,7 @@
"eslint-plugin-prettier": "^5.2.1",
"globals": "^15.9.0",
"jest": "^29.7.0",
+ "node-mocks-http": "^1.15.1",
"prettier": "^3.3.3"
},
"dependencies": {
diff --git a/src/middleware/rewriteURL.js b/src/middleware/rewriteURL.js
index a11ab2d..1d649e8 100644
--- a/src/middleware/rewriteURL.js
+++ b/src/middleware/rewriteURL.js
@@ -51,18 +51,18 @@ module.exports = (req, res, logFacilities, config, next) => {
address = address.replace(/\/+/g, "/");
tempRewrittenURL = address;
}
- if (
- matchHostname(mapEntry.host, req.headers.host) &&
- ipMatch(
- mapEntry.ip,
- req.socket ? req.socket.localAddress : undefined,
- ) &&
- address.match(createRegex(mapEntry.definingRegex)) &&
- !(mapEntry.isNotDirectory && _fileState == 2) &&
- !(mapEntry.isNotFile && _fileState == 1)
- ) {
- rewrittenURL = tempRewrittenURL;
- try {
+ try {
+ if (
+ matchHostname(mapEntry.host, req.headers.host) &&
+ ipMatch(
+ mapEntry.ip,
+ req.socket ? req.socket.localAddress : undefined,
+ ) &&
+ address.match(createRegex(mapEntry.definingRegex)) &&
+ !(mapEntry.isNotDirectory && _fileState == 2) &&
+ !(mapEntry.isNotFile && _fileState == 1)
+ ) {
+ rewrittenURL = tempRewrittenURL;
mapEntry.replacements.forEach((replacement) => {
rewrittenURL = rewrittenURL.replace(
createRegex(replacement.regex),
@@ -70,10 +70,11 @@ module.exports = (req, res, logFacilities, config, next) => {
);
});
if (mapEntry.append) rewrittenURL += mapEntry.append;
- } catch (err) {
- doCallback = false;
- callback(err, null);
+ break;
}
+ } catch (err) {
+ doCallback = false;
+ callback(err, null);
break;
}
}
diff --git a/src/middleware/staticFileServingAndDirectoryListings.js b/src/middleware/staticFileServingAndDirectoryListings.js
index 8080307..84de2b2 100644
--- a/src/middleware/staticFileServingAndDirectoryListings.js
+++ b/src/middleware/staticFileServingAndDirectoryListings.js
@@ -771,9 +771,7 @@ module.exports = (req, res, logFacilities, config, next) => {
.replace(/&/g, "&")
.replace(//g, ">")}
${
- estats.isDirectory()
- ? "-"
- : sizify(estats.size)
+ estats.isDirectory() ? "-" : sizify(estats.size)
} | ${estats.mtime.toDateString()} | \r\n`;
// Determine the file type and set the appropriate image and alt text
diff --git a/tests/middleware/blocklist.test.js b/tests/middleware/blocklist.test.js
new file mode 100644
index 0000000..a08d282
--- /dev/null
+++ b/tests/middleware/blocklist.test.js
@@ -0,0 +1,97 @@
+const middleware = require("../../src/utils/ipBlockList.js");
+const cluster = require("../../src/utils/clusterBunShim.js");
+
+jest.mock("../../src/utils/ipBlockList.js");
+jest.mock("../../src/utils/clusterBunShim.js");
+
+const ipBlockListAdd = jest.fn();
+const ipBlockListCheck = jest.fn();
+const ipBlockListRemove = jest.fn();
+
+middleware.mockImplementation(() => {
+ return {
+ check: ipBlockListCheck,
+ add: ipBlockListAdd,
+ remove: ipBlockListRemove,
+ raw: [],
+ };
+});
+
+process.serverConfig = {
+ blacklist: [],
+};
+
+const blocklistMiddleware = require("../../src/middleware/blocklist");
+
+describe("Blocklist middleware", () => {
+ let req, res, logFacilities, config, next;
+
+ beforeEach(() => {
+ req = {
+ socket: {
+ realRemoteAddress: "127.0.0.1",
+ remoteAddress: "127.0.0.1",
+ },
+ };
+ res = {
+ error: jest.fn(),
+ };
+ logFacilities = {
+ errmessage: jest.fn(),
+ };
+ config = {};
+ next = jest.fn();
+
+ cluster.isPrimary = true;
+ });
+
+ test("should call next if the IP is not in the blocklist", () => {
+ middleware().check.mockReturnValue(false);
+
+ blocklistMiddleware(req, res, logFacilities, config, next);
+
+ expect(next).toHaveBeenCalled();
+ expect(res.error).not.toHaveBeenCalled();
+ expect(logFacilities.errmessage).not.toHaveBeenCalled();
+ });
+
+ test("should call res.error if the IP is in the blocklist", () => {
+ middleware().check.mockReturnValue(true);
+
+ blocklistMiddleware(req, res, logFacilities, config, next);
+
+ expect(next).not.toHaveBeenCalled();
+ expect(res.error).toHaveBeenCalledWith(403);
+ expect(logFacilities.errmessage).toHaveBeenCalledWith(
+ "Client is in the block list.",
+ );
+ });
+
+ test("should block an IP", () => {
+ middleware().check.mockReturnValue(false);
+
+ const ip = ["192.168.1.1"];
+ const log = jest.fn();
+ const passCommand = jest.fn();
+
+ blocklistMiddleware.commands.block(ip, log, passCommand);
+
+ expect(ipBlockListAdd).toHaveBeenCalledWith("::ffff:192.168.1.1");
+ expect(process.serverConfig.blacklist).toEqual(middleware().raw);
+ expect(log).toHaveBeenCalledWith("IPs successfully blocked.");
+ expect(passCommand).toHaveBeenCalledWith(ip, log);
+ });
+
+ test("should unblock an IP", () => {
+ const ip = ["192.168.1.1"];
+ const log = jest.fn();
+ const passCommand = jest.fn();
+
+ blocklistMiddleware.commands.unblock(ip, log, passCommand);
+
+ expect(ipBlockListRemove).toHaveBeenCalledWith("::ffff:192.168.1.1");
+ expect(process.serverConfig.blacklist).toEqual(middleware().raw);
+ expect(log).toHaveBeenCalledWith("IPs successfully unblocked.");
+ expect(passCommand).toHaveBeenCalledWith(ip, log);
+ });
+});
diff --git a/tests/middleware/checkForbiddenPaths.test.js b/tests/middleware/checkForbiddenPaths.test.js
new file mode 100644
index 0000000..b8105c4
--- /dev/null
+++ b/tests/middleware/checkForbiddenPaths.test.js
@@ -0,0 +1,67 @@
+const forbiddenPaths = require("../../src/utils/forbiddenPaths.js");
+
+jest.mock("../../src/utils/forbiddenPaths.js", () => ({
+ getInitializePath: jest.fn(() => "/forbidden"),
+ isForbiddenPath: jest.fn((path) => path === "/forbidden"),
+ isIndexOfForbiddenPath: jest.fn((path) => path.includes("/forbidden")),
+ forbiddenPaths: {
+ config: "/forbidden",
+ certificates: [],
+ svrjs: "/forbidden",
+ serverSideScripts: ["/forbidden"],
+ serverSideScriptDirectories: ["/forbidden"],
+ temp: "/forbidden",
+ log: "/forbidden",
+ },
+}));
+
+process.serverConfig = {
+ secure: true,
+ sni: [],
+};
+
+process.dirname = "/usr/lib/mocksvrjs";
+process.filename = "/usr/lib/mocksvrjs/svr.js";
+
+const middleware = require("../../src/middleware/checkForbiddenPaths.js");
+
+describe("Forbidden path checking middleware", () => {
+ let req, res, logFacilities, config, next;
+
+ beforeEach(() => {
+ req = {
+ parsedURL: { pathname: "/forbidden" },
+ isProxy: false,
+ };
+ res = {
+ error: jest.fn(),
+ };
+ logFacilities = {
+ errmessage: jest.fn(),
+ };
+ config = {
+ enableLogging: true,
+ enableRemoteLogBrowsing: false,
+ exposeServerVersion: false,
+ disableServerSideScriptExpose: true,
+ };
+ next = jest.fn();
+ });
+
+ test("should deny access to forbidden paths", () => {
+ middleware(req, res, logFacilities, config, next);
+ expect(res.error).toHaveBeenCalledWith(403);
+ expect(logFacilities.errmessage).toHaveBeenCalled();
+ expect(next).not.toHaveBeenCalled();
+ });
+
+ test("should allow access to non-forbidden paths", () => {
+ req.parsedURL.pathname = "/allowed";
+ forbiddenPaths.isForbiddenPath.mockReturnValue(false);
+ forbiddenPaths.isIndexOfForbiddenPath.mockReturnValue(false);
+ middleware(req, res, logFacilities, config, next);
+ expect(res.error).not.toHaveBeenCalled();
+ expect(logFacilities.errmessage).not.toHaveBeenCalled();
+ expect(next).toHaveBeenCalled();
+ });
+});
diff --git a/tests/middleware/defaultHandlerChecks.test.js b/tests/middleware/defaultHandlerChecks.test.js
new file mode 100644
index 0000000..b8d4756
--- /dev/null
+++ b/tests/middleware/defaultHandlerChecks.test.js
@@ -0,0 +1,54 @@
+const middleware = require("../../src/middleware/defaultHandlerChecks.js");
+const httpMocks = require("node-mocks-http");
+
+describe("Default handler checks middleware", () => {
+ let req, res, logFacilities, config, next;
+
+ beforeEach(() => {
+ req = httpMocks.createRequest();
+ res = httpMocks.createResponse();
+ logFacilities = {
+ errmessage: jest.fn(),
+ };
+ config = {
+ getCustomHeaders: jest.fn(() => ({})),
+ generateServerString: jest.fn(() => "Server String"),
+ };
+ next = jest.fn();
+ });
+
+ test("should return 501 and log error message if req.isProxy is true", () => {
+ req.isProxy = true;
+ middleware(req, res, logFacilities, config, next);
+ expect(res._getStatusCode()).toBe(501);
+ expect(logFacilities.errmessage).toHaveBeenCalledWith(
+ expect.stringContaining("doesn't support proxy without proxy mod."),
+ );
+ });
+
+ test("should return 204 if req.method is OPTIONS", () => {
+ req.method = "OPTIONS";
+ middleware(req, res, logFacilities, config, next);
+ expect(res._getStatusCode()).toBe(204);
+ expect(res._getHeaders()).toHaveProperty(
+ "allow",
+ "GET, POST, HEAD, OPTIONS",
+ );
+ });
+
+ test("should call res.error with 405 and log error message if req.method is not GET, POST, or HEAD", () => {
+ req.method = "PUT";
+ res.error = jest.fn();
+ middleware(req, res, logFacilities, config, next);
+ expect(res.error).toHaveBeenCalledWith(405);
+ expect(logFacilities.errmessage).toHaveBeenCalledWith(
+ expect.stringContaining("Invaild method: PUT"),
+ );
+ });
+
+ test("should call next if req.method is GET, POST, or HEAD and req.isProxy is false", () => {
+ req.method = "GET";
+ middleware(req, res, logFacilities, config, next);
+ expect(next).toHaveBeenCalled();
+ });
+});
diff --git a/tests/middleware/nonStandardCodesAndHttpAuthentication.test.js b/tests/middleware/nonStandardCodesAndHttpAuthentication.test.js
new file mode 100644
index 0000000..2c7df98
--- /dev/null
+++ b/tests/middleware/nonStandardCodesAndHttpAuthentication.test.js
@@ -0,0 +1,281 @@
+const sha256 = require("../../src/utils/sha256.js");
+const ipMatch = require("../../src/utils/ipMatch.js");
+const matchHostname = require("../../src/utils/matchHostname.js");
+const ipBlockList = require("../../src/utils/ipBlockList.js");
+const cluster = require("../../src/utils/clusterBunShim.js");
+
+jest.mock("../../src/utils/sha256.js");
+jest.mock("../../src/utils/ipMatch.js");
+jest.mock("../../src/utils/matchHostname.js");
+jest.mock("../../src/utils/ipBlockList.js");
+jest.mock("../../src/utils/clusterBunShim.js");
+
+let mockScryptHash = "mocked-scrypt-hash";
+let mockPbkdf2Hash = "mocked-pbkdf2-hash";
+
+jest.mock("crypto", () => {
+ return {
+ scrypt: jest.fn((password, salt, keylen, callback) => {
+ // Mock implementation for crypto.scrypt
+ callback(null, Buffer.from(mockScryptHash));
+ }),
+ pbkdf2: jest.fn((password, salt, iterations, keylen, digest, callback) => {
+ // Mock implementation for crypto.pbkdf2
+ callback(null, Buffer.from(mockPbkdf2Hash));
+ }),
+ // Add other properties or methods of crypto module if needed
+ };
+});
+
+process.serverConfig = {
+ nonStandardCodes: [
+ {
+ host: "example.com",
+ ip: "192.168.1.1",
+ url: "/test/path",
+ scode: 403,
+ users: ["127.0.0.1"],
+ },
+ {
+ host: "example.com",
+ ip: "192.168.1.1",
+ url: "/test/path2",
+ scode: 401,
+ },
+ ],
+};
+
+process.messageEventListeners = [];
+
+process.send = undefined;
+
+const middleware = require("../../src/middleware/nonStandardCodesAndHttpAuthentication.js");
+
+describe("Non-standard codes and HTTP authentication middleware", () => {
+ let req, res, logFacilities, config, next;
+
+ beforeEach(() => {
+ req = {
+ socket: {
+ realRemoteAddress: "127.0.0.1",
+ localAddress: "192.168.1.1",
+ },
+ parsedURL: {
+ pathname: "/test/path",
+ },
+ url: "/test/path",
+ headers: {
+ host: "example.com",
+ },
+ isProxy: false,
+ };
+ res = {
+ error: jest.fn(),
+ redirect: jest.fn(),
+ };
+ logFacilities = {
+ errmessage: jest.fn(),
+ reqmessage: jest.fn(),
+ };
+ config = {
+ getCustomHeaders: jest.fn(),
+ users: [],
+ };
+ next = jest.fn();
+ process.serverConfig = {
+ nonStandardCodes: [],
+ };
+
+ cluster.isPrimary = true;
+ config.getCustomHeaders.mockReturnValue({});
+ });
+
+ test("should handle non-standard codes", () => {
+ ipBlockList.mockReturnValue({
+ check: jest.fn().mockReturnValue(true),
+ });
+ matchHostname.mockReturnValue(true);
+ ipMatch.mockReturnValue(true);
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(res.error).toHaveBeenCalledWith(403);
+ expect(logFacilities.errmessage).toHaveBeenCalledWith("Content blocked.");
+ });
+
+ test("should handle HTTP authentication", () => {
+ req.parsedURL.pathname = "/test/path2";
+ req.url = "/test/path2";
+ matchHostname.mockReturnValue(true);
+ ipMatch.mockReturnValue(true);
+ config.users = [
+ {
+ name: "test",
+ pass: "test",
+ salt: "test",
+ },
+ ];
+ sha256.mockReturnValue("test");
+ req.headers.authorization = "Basic dGVzdDp0ZXN0";
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(next).toHaveBeenCalled();
+ expect(logFacilities.reqmessage).toHaveBeenCalledWith(
+ 'Client is logged in as "test".',
+ );
+ });
+
+ test("should handle brute force protection", () => {
+ req.parsedURL.pathname = "/test/path2";
+ req.url = "/test/path2";
+ req.socket.realRemoteAddress = "127.0.0.2";
+ matchHostname.mockReturnValue(true);
+ ipMatch.mockReturnValue(true);
+ config.users = [
+ {
+ name: "test",
+ pass: "test2",
+ salt: "test",
+ },
+ ];
+ sha256.mockReturnValue("test");
+ req.headers.authorization = "Basic dGVzdDp0ZXN0";
+
+ // Maximum 10 login attempts by default
+ for (let i = 0; i < 11; i++) {
+ logFacilities.errmessage.mockClear();
+ middleware(req, res, logFacilities, config, next);
+ }
+
+ expect(next).not.toHaveBeenCalled();
+ expect(logFacilities.errmessage).toHaveBeenCalledWith(
+ "Brute force limit reached!",
+ );
+ });
+
+ test("should handle HTTP authentication with scrypt", () => {
+ req.parsedURL.pathname = "/test/path2";
+ req.url = "/test/path2";
+ matchHostname.mockReturnValue(true);
+ ipMatch.mockReturnValue(true);
+ config.users = [
+ {
+ name: "test",
+ pass: "74657374", // "test" converted to hex
+ salt: "test",
+ scrypt: true,
+ },
+ ];
+ mockScryptHash = "test";
+ req.headers.authorization = "Basic dGVzdDp0ZXN0";
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(next).toHaveBeenCalled();
+ expect(logFacilities.reqmessage).toHaveBeenCalledWith(
+ 'Client is logged in as "test".',
+ );
+ });
+
+ test("should handle HTTP authentication with PBKDF2", () => {
+ req.parsedURL.pathname = "/test/path2";
+ req.url = "/test/path2";
+ matchHostname.mockReturnValue(true);
+ ipMatch.mockReturnValue(true);
+ config.users = [
+ {
+ name: "test",
+ pass: "74657374", // "test" converted to hex
+ salt: "test",
+ pbkdf2: true,
+ },
+ ];
+ mockPbkdf2Hash = "test";
+ req.headers.authorization = "Basic dGVzdDp0ZXN0";
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(next).toHaveBeenCalled();
+ expect(logFacilities.reqmessage).toHaveBeenCalledWith(
+ 'Client is logged in as "test".',
+ );
+ });
+
+ test("should call next if no non-standard codes or HTTP authentication is needed", () => {
+ req.parsedURL.pathname = "/test/path3";
+ req.url = "/test/path3";
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should handle HTTP authentication with clustering", () => {
+ cluster.isPrimary = false;
+ req.parsedURL.pathname = "/test/path2";
+ req.url = "/test/path2";
+ matchHostname.mockReturnValue(true);
+ ipMatch.mockReturnValue(true);
+ config.users = [
+ {
+ name: "test",
+ pass: "test",
+ salt: "test",
+ },
+ ];
+ sha256.mockReturnValue("test");
+ req.headers.authorization = "Basic dGVzdDp0ZXN0";
+ let mockHandlers = [];
+ process.on = (eventType, eventListener) => {
+ if (eventType == "message") {
+ mockHandlers.push(eventListener);
+ }
+ };
+ process.once = (eventType, eventListener) => {
+ const wrap = (...params) => {
+ eventListener(...params);
+ process.removeListener(eventType, wrap);
+ };
+ process.on(eventType, wrap);
+ };
+ process.addListener = process.on;
+ process.removeListener = (eventType, eventListener) => {
+ if (eventType == "message") {
+ let indexOfListener = mockHandlers.indexOf(eventListener);
+ if (indexOfListener != -1) mockHandlers.splice(indexOfListener, 1);
+ }
+ };
+ process.removeAllListeners = (eventType) => {
+ if (eventType == "message") {
+ mockHandlers = [];
+ }
+ };
+ process.send = (message) => {
+ const mockWorker = {
+ send: (msg) => {
+ mockHandlers.forEach((handler) => handler(msg));
+ },
+ };
+ const mockServerConsole = {
+ climessage: () => {},
+ reqmessage: () => {},
+ resmessage: () => {},
+ errmessage: () => {},
+ locerrmessage: () => {},
+ locwarnmessage: () => {},
+ locmessage: () => {},
+ };
+ process.messageEventListeners.forEach((listenerWrapper) =>
+ listenerWrapper(mockWorker, mockServerConsole)(message),
+ );
+ };
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(next).toHaveBeenCalled();
+ expect(logFacilities.reqmessage).toHaveBeenCalledWith(
+ 'Client is logged in as "test".',
+ );
+ });
+});
diff --git a/tests/middleware/redirectTrailingSlashes.test.js b/tests/middleware/redirectTrailingSlashes.test.js
new file mode 100644
index 0000000..22e7eb2
--- /dev/null
+++ b/tests/middleware/redirectTrailingSlashes.test.js
@@ -0,0 +1,91 @@
+const fs = require("fs");
+const middleware = require("../../src/middleware/redirectTrailingSlashes.js");
+
+jest.mock("fs");
+
+describe("Trailing slash redirection middleware", () => {
+ let req, res, logFacilities, config, next;
+
+ beforeEach(() => {
+ req = {
+ isProxy: false,
+ parsedURL: { pathname: "/test", search: "?query=1", hash: "#hash" },
+ originalParsedURL: { pathname: "/test" },
+ };
+ res = {
+ redirect: jest.fn(),
+ error: jest.fn(),
+ };
+ logFacilities = {};
+ config = { disableTrailingSlashRedirects: false };
+ next = jest.fn();
+ });
+
+ test("should redirect if pathname does not end with a slash", () => {
+ fs.stat.mockImplementation((path, cb) => {
+ cb(null, { isDirectory: () => true });
+ });
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(res.redirect).toHaveBeenCalledWith("/test/?query=1#hash");
+ });
+
+ test("should not redirect if pathname ends with a slash", () => {
+ req.parsedURL.pathname = "/test/";
+ req.originalParsedURL.pathname = "/test/";
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should not redirect if disableTrailingSlashRedirects is true", () => {
+ config.disableTrailingSlashRedirects = true;
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should not redirect if isProxy is true", () => {
+ req.isProxy = true;
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should call next if fs.stat returns an error", () => {
+ fs.stat.mockImplementation((path, cb) => {
+ cb(new Error("File does not exist"));
+ });
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should call next if fs.stat returns a file that is not a directory", () => {
+ fs.stat.mockImplementation((path, cb) => {
+ cb(null, { isDirectory: () => false });
+ });
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should call res.error if next throws an error", () => {
+ fs.stat.mockImplementation((path, cb) => {
+ cb(null, { isDirectory: () => false });
+ });
+ next.mockImplementation(() => {
+ throw new Error("Next error");
+ });
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(res.error).toHaveBeenCalledWith(500, new Error("Next error"));
+ });
+});
diff --git a/tests/middleware/redirects.test.js b/tests/middleware/redirects.test.js
new file mode 100644
index 0000000..9cb5700
--- /dev/null
+++ b/tests/middleware/redirects.test.js
@@ -0,0 +1,52 @@
+const middleware = require("../../src/middleware/redirects.js");
+
+describe("Redirects middleware", () => {
+ let req, res, logFacilities, config, next;
+
+ beforeEach(() => {
+ req = {
+ headers: {},
+ socket: { encrypted: false, remoteAddress: "8.8.8.8" },
+ isProxy: false,
+ url: "/test",
+ };
+ res = {
+ redirect: jest.fn(),
+ error: jest.fn(),
+ };
+ logFacilities = {
+ errmessage: jest.fn(),
+ };
+ config = {
+ secure: true,
+ disableNonEncryptedServer: false,
+ disableToHTTPSRedirect: false,
+ port: 80,
+ sport: 443,
+ spubport: 8443,
+ wwwredirect: true,
+ domain: "example.com",
+ };
+ next = jest.fn();
+ });
+
+ test("should redirect to HTTPS if config.secure is true and connection is not encrypted", () => {
+ req.headers.host = "www.example.com";
+ middleware(req, res, logFacilities, config, next);
+ expect(res.redirect).toHaveBeenCalledWith("https://www.example.com/test");
+ });
+
+ test("should not redirect if connection is encrypted", () => {
+ req.headers.host = "www.example.com";
+ req.socket.encrypted = true;
+ middleware(req, res, logFacilities, config, next);
+ expect(res.redirect).not.toHaveBeenCalled();
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should redirect to www subdomain if config.wwwredirect is true and host does not start with www", () => {
+ req.headers.host = "example.com";
+ middleware(req, res, logFacilities, config, next);
+ expect(res.redirect).toHaveBeenCalledWith("https://example.com/test");
+ });
+});
diff --git a/tests/middleware/responseHeaders.test.js b/tests/middleware/responseHeaders.test.js
new file mode 100644
index 0000000..51c942d
--- /dev/null
+++ b/tests/middleware/responseHeaders.test.js
@@ -0,0 +1,48 @@
+const middleware = require("../../src/middleware/responseHeaders.js");
+
+describe("Response header setting middleware", () => {
+ let req, res, next, config, logFacilities;
+
+ beforeEach(() => {
+ req = { isProxy: false };
+ res = { setHeader: jest.fn() };
+ next = jest.fn();
+ config = {
+ getCustomHeaders: jest.fn(() => ({ "X-Custom-Header": "custom-value" })),
+ };
+ logFacilities = {};
+ });
+
+ test("should set custom headers if req.isProxy is false", () => {
+ middleware(req, res, logFacilities, config, next);
+
+ expect(res.setHeader).toHaveBeenCalledWith(
+ "X-Custom-Header",
+ "custom-value",
+ );
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should not set custom headers if req.isProxy is true", () => {
+ req.isProxy = true;
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(res.setHeader).not.toHaveBeenCalled();
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should call next even if an error occurs while setting headers", () => {
+ res.setHeader.mockImplementation(() => {
+ throw new Error("test error");
+ });
+
+ middleware(req, res, logFacilities, config, next);
+
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should have proxySafe property set to true", () => {
+ expect(middleware.proxySafe).toBe(true);
+ });
+});
diff --git a/tests/middleware/rewriteURL.test.js b/tests/middleware/rewriteURL.test.js
new file mode 100644
index 0000000..13b3c22
--- /dev/null
+++ b/tests/middleware/rewriteURL.test.js
@@ -0,0 +1,143 @@
+const middleware = require("../../src/middleware/rewriteURL.js");
+const createRegex = require("../../src/utils/createRegex.js");
+const sanitizeURL = require("../../src/utils/urlSanitizer.js");
+const parseURL = require("../../src/utils/urlParser.js");
+
+jest.mock("fs");
+jest.mock("../../src/utils/urlSanitizer.js");
+jest.mock("../../src/utils/urlParser.js");
+jest.mock("../../src/utils/createRegex.js");
+
+describe("rewriteURL middleware", () => {
+ let req, res, logFacilities, config, next;
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ req = {
+ parsedURL: {
+ pathname: "/test",
+ search: "",
+ hash: "",
+ },
+ url: "/test",
+ headers: {
+ host: "test.com",
+ },
+ socket: {
+ encrypted: false,
+ localAddress: "127.0.0.1",
+ },
+ };
+ res = {
+ error: jest.fn(),
+ };
+ logFacilities = {
+ resmessage: jest.fn(),
+ errmessage: jest.fn(),
+ };
+ config = {
+ rewriteMap: [],
+ domain: "test.com",
+ allowDoubleSlashes: false,
+ };
+ next = jest.fn();
+
+ // Make mocks call actual functions
+ createRegex.mockImplementation((...params) =>
+ jest.requireActual("../../src/utils/createRegex.js")(...params),
+ );
+ parseURL.mockImplementation((...params) =>
+ jest.requireActual("../../src/utils/urlParser.js")(...params),
+ );
+ sanitizeURL.mockImplementation((...params) =>
+ jest.requireActual("../../src/utils/urlSanitizer.js")(...params),
+ );
+ });
+
+ test("should call next if URL is not rewritten", () => {
+ middleware(req, res, logFacilities, config, next);
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should return 400 if URL decoding fails", () => {
+ req.parsedURL.pathname = "%";
+ middleware(req, res, logFacilities, config, next);
+ expect(res.error).toHaveBeenCalledWith(400);
+ });
+
+ test("should return 500 if rewriteURL callback returns an error", () => {
+ config.rewriteMap = [
+ {
+ host: "test.com",
+ definingRegex: "/.*/",
+ replacements: [
+ {
+ regex: "/.*/",
+ replacement: "error",
+ },
+ ],
+ },
+ ];
+ createRegex.mockImplementation(() => {
+ throw new Error("Test error");
+ });
+ middleware(req, res, logFacilities, config, next);
+ expect(res.error).toHaveBeenCalledWith(500, expect.any(Error));
+ });
+
+ test("should return 400 if parsedURL is invalid", () => {
+ config.rewriteMap = [
+ {
+ host: "test.com",
+ definingRegex: "/.*/",
+ replacements: [
+ {
+ regex: "/.*/",
+ replacement: "/new",
+ },
+ ],
+ },
+ ];
+ parseURL.mockImplementation(() => {
+ throw new Error("Test error");
+ });
+ middleware(req, res, logFacilities, config, next);
+ expect(res.error).toHaveBeenCalledWith(400, expect.any(Error));
+ });
+
+ test("should return 403 if URL is sanitized", () => {
+ config.rewriteMap = [
+ {
+ host: "test.com",
+ definingRegex: "/.*/",
+ replacements: [
+ {
+ regex: "/.*/",
+ replacement: "/new",
+ },
+ ],
+ },
+ ];
+ sanitizeURL.mockReturnValue("/sanitized");
+ middleware(req, res, logFacilities, config, next);
+ expect(res.error).toHaveBeenCalledWith(403);
+ expect(logFacilities.errmessage).toHaveBeenCalledWith("Content blocked.");
+ });
+
+ test("should call next if URL is rewritten successfully", () => {
+ config.rewriteMap = [
+ {
+ host: "test.com",
+ definingRegex: "/.*/",
+ replacements: [
+ {
+ regex: "/.*/",
+ replacement: "/new",
+ },
+ ],
+ },
+ ];
+ middleware(req, res, logFacilities, config, next);
+ expect(next).toHaveBeenCalled();
+ });
+});
diff --git a/tests/middleware/staticFileServingAndDirectoryListing.test.js b/tests/middleware/staticFileServingAndDirectoryListing.test.js
new file mode 100644
index 0000000..759e587
--- /dev/null
+++ b/tests/middleware/staticFileServingAndDirectoryListing.test.js
@@ -0,0 +1,185 @@
+const middleware = require("../../src/middleware/staticFileServingAndDirectoryListings.js");
+const fs = require("fs");
+const http = require("http");
+const httpMocks = require("node-mocks-http");
+
+jest.mock("fs");
+
+describe("Static file serving and directory listings middleware", () => {
+ let req, res, logFacilities, config, next;
+
+ beforeEach(() => {
+ req = httpMocks.createRequest({
+ method: "GET",
+ url: "/",
+ headers: {
+ host: "example.com",
+ "accept-encoding": "gzip, deflate, br",
+ "user-agent": "Mozilla/5.0",
+ },
+ socket: {
+ localAddress: "127.0.0.1",
+ },
+ });
+ req.parsedURL = {
+ pathname: "/",
+ };
+ req.originalParsedURL = {
+ pathname: "/",
+ };
+ res = httpMocks.createResponse({
+ eventEmitter: require("events").EventEmitter,
+ });
+ res.error = (statusCode) => {
+ // Very simple replacement of res.error
+ res.writeHead(statusCode, { "Content-Type": "text/plain" });
+ res.end(statusCode + " " + http.STATUS_CODES[statusCode]);
+ };
+ logFacilities = {
+ errmessage: jest.fn(),
+ resmessage: jest.fn(),
+ };
+ config = {
+ enableDirectoryListing: true,
+ enableDirectoryListingVHost: [],
+ enableCompression: true,
+ dontCompress: [],
+ generateServerString: jest.fn().mockReturnValue("Server"),
+ };
+ next = jest.fn();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("should return 404 if file does not exist", async () => {
+ fs.stat.mockImplementation((path, cb) => {
+ cb({ code: "ENOENT" });
+ });
+
+ await middleware(req, res, logFacilities, config, next);
+
+ expect(res.statusCode).toBe(404);
+ expect(logFacilities.errmessage).toHaveBeenCalledWith(
+ "Resource not found.",
+ );
+ });
+
+ test("should return 403 if directory listing is disabled", async () => {
+ config.enableDirectoryListing = false;
+ fs.stat.mockImplementation((path, cb) => {
+ cb(null, { isDirectory: () => true, isFile: () => false });
+ });
+
+ await middleware(req, res, logFacilities, config, next);
+
+ expect(res.statusCode).toBe(403);
+ expect(logFacilities.errmessage).toHaveBeenCalledWith(
+ "Directory listing is disabled.",
+ );
+ });
+
+ test("should return 403 if access is denied", async () => {
+ fs.stat.mockImplementation((path, cb) => {
+ cb({ code: "EACCES" });
+ });
+
+ await middleware(req, res, logFacilities, config, next);
+
+ expect(res.statusCode).toBe(403);
+ expect(logFacilities.errmessage).toHaveBeenCalledWith("Access denied.");
+ });
+
+ test("should return 414 if the URI is too long", async () => {
+ fs.stat.mockImplementation((path, cb) => {
+ cb({ code: "ENAMETOOLONG" });
+ });
+
+ await middleware(req, res, logFacilities, config, next);
+
+ expect(res.statusCode).toBe(414);
+ });
+
+ test("should return 503 if the server is unable to handle the request", async () => {
+ fs.stat.mockImplementation((path, cb) => {
+ cb({ code: "EMFILE" });
+ });
+
+ await middleware(req, res, logFacilities, config, next);
+
+ expect(res.statusCode).toBe(503);
+ });
+
+ test("should return 508 if a loop is detected in symbolic links", async () => {
+ fs.stat.mockImplementation((path, cb) => {
+ cb({ code: "ELOOP" });
+ });
+
+ await middleware(req, res, logFacilities, config, next);
+
+ expect(res.statusCode).toBe(508);
+ expect(logFacilities.errmessage).toHaveBeenCalledWith(
+ "Symbolic link loop detected.",
+ );
+ });
+
+ test("should return 500 if an unknown error occurs", async () => {
+ fs.stat.mockImplementation((path, cb) => {
+ cb(new Error("Unknown error"));
+ });
+
+ await middleware(req, res, logFacilities, config, next);
+
+ expect(res.statusCode).toBe(500);
+ });
+
+ test("should return 501 if the file is a block device, character device, FIFO, or socket", async () => {
+ fs.stat.mockImplementation((path, cb) => {
+ cb(null, {
+ isDirectory: () => false,
+ isFile: () => false,
+ isBlockDevice: () => true,
+ });
+ });
+
+ await middleware(req, res, logFacilities, config, next);
+
+ expect(res.statusCode).toBe(501);
+ expect(logFacilities.errmessage).toHaveBeenCalledWith(
+ expect.stringContaining("doesn't support block devices"),
+ );
+ });
+
+ test("should return a directory listing if the path is a directory and directory listing is enabled", async () => {
+ fs.readdir.mockImplementation((path, cb) => {
+ cb(null, ["file1.txt", "file2.txt"]);
+ });
+ fs.readFile.mockImplementation((path, cb) => {
+ if (path.match(/(?:^|\/)file[12]\.txt$/)) {
+ cb(null, Buffer.from("test"));
+ } else {
+ cb({ code: "ENOENT" });
+ }
+ });
+ fs.stat.mockImplementation((path, cb) => {
+ if (!path.match(/(?:^|\/)file[12]\.txt$/)) {
+ cb(null, { isDirectory: () => true, isFile: () => false });
+ } else {
+ cb(null, {
+ isDirectory: () => false,
+ isFile: () => true,
+ size: 1024,
+ mtime: new Date(),
+ });
+ }
+ });
+
+ await middleware(req, res, logFacilities, config, next);
+
+ expect(res.statusCode).toBe(200);
+ expect(res._getData()).toContain("Directory: /");
+ expect(res._getData()).toContain("file1.txt");
+ expect(res._getData()).toContain("file2.txt");
+ });
+});
diff --git a/tests/middleware/status.test.js b/tests/middleware/status.test.js
new file mode 100644
index 0000000..a5824be
--- /dev/null
+++ b/tests/middleware/status.test.js
@@ -0,0 +1,77 @@
+const middleware = require("../../src/middleware/status.js");
+const http = require("http");
+const os = require("os");
+
+describe("Status middleware", () => {
+ let req, res, logFacilities, config, next;
+
+ beforeEach(() => {
+ req = {
+ parsedURL: { pathname: "/svrjsstatus.svr" },
+ headers: { host: "localhost" },
+ };
+ res = {
+ writeHead: jest.fn(),
+ end: jest.fn(),
+ head: "",
+ foot: "",
+ };
+ logFacilities = {};
+ config = {
+ allowStatus: true,
+ generateServerString: () => "Test Server",
+ };
+ next = jest.fn();
+ process.reqcounter = 100;
+ process.err4xxcounter = 10;
+ process.err5xxcounter = 5;
+ process.malformedcounter = 2;
+ process.uptime = jest.fn(() => 1000);
+ process.memoryUsage = jest.fn(() => ({ rss: 1024 }));
+ process.cpuUsage = jest.fn(() => ({ user: 500000, system: 500000 }));
+ process.pid = 1234;
+ });
+
+ test("should set response headers and body when conditions are met", () => {
+ middleware(req, res, logFacilities, config, next);
+ expect(res.writeHead).toHaveBeenCalledWith(200, http.STATUS_CODES[200], {
+ "Content-Type": "text/html; charset=utf-8",
+ });
+ expect(res.end).toHaveBeenCalled();
+ });
+
+ test("should call next function when conditions are not met", () => {
+ req.parsedURL.pathname = "/";
+ middleware(req, res, logFacilities, config, next);
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should handle case insensitivity on Windows", () => {
+ req.parsedURL.pathname = "/SvrJsStatus.Svr";
+ jest.spyOn(os, "platform").mockReturnValue("win32");
+ middleware(req, res, logFacilities, config, next);
+ expect(res.writeHead).toHaveBeenCalledWith(200, http.STATUS_CODES[200], {
+ "Content-Type": "text/html; charset=utf-8",
+ });
+ expect(res.end).toHaveBeenCalled();
+ os.platform.mockRestore();
+ });
+
+ test("should handle undefined host header", () => {
+ req.headers.host = undefined;
+ middleware(req, res, logFacilities, config, next);
+ expect(res.writeHead).toHaveBeenCalledWith(200, http.STATUS_CODES[200], {
+ "Content-Type": "text/html; charset=utf-8",
+ });
+ expect(res.end).toHaveBeenCalled();
+ });
+
+ test("should handle custom head and foot", () => {
+ const headContents = "";
+ res.head = `${headContents}`;
+ res.foot = "";
+ middleware(req, res, logFacilities, config, next);
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining(headContents));
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining(res.foot));
+ });
+});
diff --git a/tests/middleware/urlSanitizer.test.js b/tests/middleware/urlSanitizer.test.js
new file mode 100644
index 0000000..975004d
--- /dev/null
+++ b/tests/middleware/urlSanitizer.test.js
@@ -0,0 +1,99 @@
+const middleware = require("../../src/middleware/urlSanitizer.js");
+const sanitizeURL = require("../../src/utils/urlSanitizer.js");
+const parseURL = require("../../src/utils/urlParser.js");
+
+jest.mock("../../src/utils/urlSanitizer.js");
+jest.mock("../../src/utils/urlParser.js");
+
+describe("Path sanitizer middleware", () => {
+ let req, res, logFacilities, config, next;
+
+ beforeEach(() => {
+ req = {
+ parsedURL: {
+ pathname: "/test",
+ search: "?query=test",
+ hash: "#hash",
+ },
+ url: "/test?query=test#hash",
+ isProxy: false,
+ headers: {
+ host: "test.com",
+ },
+ socket: {
+ encrypted: false,
+ },
+ };
+ res = {
+ redirect: jest.fn(),
+ error: jest.fn(),
+ };
+ logFacilities = {
+ resmessage: jest.fn(),
+ };
+ config = {
+ allowDoubleSlashes: false,
+ rewriteDirtyURLs: false,
+ domain: "test.com",
+ };
+ next = jest.fn();
+
+ sanitizeURL.mockImplementation((url) => url);
+ parseURL.mockImplementation((url) => ({ pathname: url }));
+ });
+
+ test("should call next if URL is not dirty", () => {
+ middleware(req, res, logFacilities, config, next);
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should redirect if URL is dirty and rewriteDirtyURLs is false", () => {
+ req.parsedURL.pathname = "/dirty%20url";
+ middleware(req, res, logFacilities, config, next);
+ expect(res.redirect).toHaveBeenCalledWith(
+ "/dirty%20url?query=test#hash",
+ false,
+ );
+ expect(next).not.toHaveBeenCalled();
+ });
+
+ test("should rewrite URL if URL is dirty and rewriteDirtyURLs is true", () => {
+ req.parsedURL.pathname = "/dirty%20url";
+ config.rewriteDirtyURLs = true;
+ middleware(req, res, logFacilities, config, next);
+ expect(req.url).toBe("/dirty%20url?query=test#hash");
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should redirect if URL is dirty (sanitized via sanitizeURL) and rewriteDirtyURLs is false", () => {
+ req.parsedURL.pathname = "/dirty%20url";
+ sanitizeURL.mockImplementation((url) => url.replace(/dirty/g, "clean"));
+ middleware(req, res, logFacilities, config, next);
+ expect(res.redirect).toHaveBeenCalledWith(
+ "/clean%20url?query=test#hash",
+ false,
+ );
+ expect(next).not.toHaveBeenCalled();
+ });
+
+ test("should rewrite URL if URL is dirty (sanitized via sanitizeURL) and rewriteDirtyURLs is true", () => {
+ req.parsedURL.pathname = "/dirty%20url";
+ config.rewriteDirtyURLs = true;
+ sanitizeURL.mockImplementation((url) => url.replace(/dirty/g, "clean"));
+ middleware(req, res, logFacilities, config, next);
+ expect(req.url).toBe("/clean%20url?query=test#hash");
+ expect(next).toHaveBeenCalled();
+ });
+
+ test("should handle parseURL errors", () => {
+ req.parsedURL.pathname = "/dirty%20url";
+ config.rewriteDirtyURLs = true;
+ sanitizeURL.mockImplementation((url) => url.replace(/dirty/g, "clean"));
+ parseURL.mockImplementation(() => {
+ throw new Error("Parse error");
+ });
+ middleware(req, res, logFacilities, config, next);
+ expect(res.error).toHaveBeenCalledWith(400, new Error("Parse error"));
+ expect(next).not.toHaveBeenCalled();
+ });
+});
diff --git a/tests/middleware/webRootPrefixes.test.js b/tests/middleware/webRootPrefixes.test.js
new file mode 100644
index 0000000..3d2447f
--- /dev/null
+++ b/tests/middleware/webRootPrefixes.test.js
@@ -0,0 +1,68 @@
+const middleware = require("../../src/middleware/webRootPostfixes.js");
+const createRegex = require("../../src/utils/createRegex.js");
+const ipMatch = require("../../src/utils/ipMatch.js");
+const sanitizeURL = require("../../src/utils/urlSanitizer.js");
+const parseURL = require("../../src/utils/urlParser.js");
+
+jest.mock("../../src/utils/createRegex.js");
+jest.mock("../../src/utils/ipMatch.js");
+jest.mock("../../src/utils/urlSanitizer.js");
+jest.mock("../../src/utils/urlParser.js");
+
+describe("Web root postfixes middleware", () => {
+ let req, res, logFacilities, config, next;
+
+ beforeEach(() => {
+ req = {
+ isProxy: false,
+ url: "/test",
+ parsedURL: { pathname: "/test" },
+ headers: { host: "test.com" },
+ socket: { localAddress: "127.0.0.1" },
+ };
+ res = { error: jest.fn() };
+ logFacilities = { resmessage: jest.fn(), errmessage: jest.fn() };
+ config = {
+ allowPostfixDoubleSlashes: true,
+ wwwrootPostfixPrefixesVHost: [],
+ wwwrootPostfixesVHost: [
+ { host: "test.com", ip: "127.0.0.1", postfix: "postfix" },
+ ],
+ };
+ next = jest.fn();
+
+ createRegex.mockReturnValue(new RegExp());
+ ipMatch.mockReturnValue(true);
+ sanitizeURL.mockImplementation((url) => url);
+ parseURL.mockImplementation((url) => ({ pathname: url }));
+ });
+
+ test("should add web root postfix", () => {
+ middleware(req, res, logFacilities, config, next);
+ expect(req.url).toBe("/postfix/test");
+ expect(logFacilities.resmessage).toHaveBeenCalledWith(
+ "Added web root postfix: /test => /postfix/test",
+ );
+ });
+
+ test("should not add web root postfix if req.isProxy is true", () => {
+ req.isProxy = true;
+ middleware(req, res, logFacilities, config, next);
+ expect(req.url).toBe("/test");
+ expect(logFacilities.resmessage).not.toHaveBeenCalled();
+ });
+
+ test("should not add web root postfix if no matching config is found", () => {
+ config.wwwrootPostfixesVHost = [
+ { host: "example.com", ip: "127.0.0.1", postfix: "postfix" },
+ ];
+ middleware(req, res, logFacilities, config, next);
+ expect(req.url).toBe("/test");
+ expect(logFacilities.resmessage).not.toHaveBeenCalled();
+ });
+
+ test("should call next function", () => {
+ middleware(req, res, logFacilities, config, next);
+ expect(next).toHaveBeenCalled();
+ });
+});
diff --git a/tests/utils/createRegex.test.js b/tests/utils/createRegex.test.js
index 8209874..a61562b 100644
--- a/tests/utils/createRegex.test.js
+++ b/tests/utils/createRegex.test.js
@@ -1,4 +1,4 @@
-const createRegex = require("../../src/utils/createRegex");
+const createRegex = require("../../src/utils/createRegex.js");
const os = require("os");
jest.mock("os", () => ({
diff --git a/tests/utils/deepClone.test.js b/tests/utils/deepClone.test.js
index 8bcbc1c..d288b54 100644
--- a/tests/utils/deepClone.test.js
+++ b/tests/utils/deepClone.test.js
@@ -1,4 +1,4 @@
-const deepClone = require("../../src/utils/deepClone");
+const deepClone = require("../../src/utils/deepClone.js");
describe("Deep cloning function", () => {
test("should clone a simple object", () => {
diff --git a/tests/utils/forbiddenPaths.test.js b/tests/utils/forbiddenPaths.test.js
index 1badecb..7e0372b 100644
--- a/tests/utils/forbiddenPaths.test.js
+++ b/tests/utils/forbiddenPaths.test.js
@@ -3,7 +3,7 @@ const {
isForbiddenPath,
isIndexOfForbiddenPath,
forbiddenPaths,
-} = require("../../src/utils/forbiddenPaths");
+} = require("../../src/utils/forbiddenPaths.js");
const os = require("os");
jest.mock("os", () => ({
diff --git a/tests/utils/generateErrorStack.test.js b/tests/utils/generateErrorStack.test.js
index 0f14973..64e218c 100644
--- a/tests/utils/generateErrorStack.test.js
+++ b/tests/utils/generateErrorStack.test.js
@@ -1,4 +1,4 @@
-const generateErrorStack = require("../../src/utils/generateErrorStack");
+const generateErrorStack = require("../../src/utils/generateErrorStack.js");
describe("Error stack generation function", () => {
test("should return the original stack if it is V8-style", () => {
diff --git a/tests/utils/ipBlockList.test.js b/tests/utils/ipBlockList.test.js
index 92c5f8b..7446bdc 100644
--- a/tests/utils/ipBlockList.test.js
+++ b/tests/utils/ipBlockList.test.js
@@ -1,4 +1,4 @@
-const ipBlockList = require("../../src/utils/ipBlockList");
+const ipBlockList = require("../../src/utils/ipBlockList.js");
describe("IP block list functionality", () => {
let blockList;
diff --git a/tests/utils/ipMatch.test.js b/tests/utils/ipMatch.test.js
index f8e8c87..28c5132 100644
--- a/tests/utils/ipMatch.test.js
+++ b/tests/utils/ipMatch.test.js
@@ -1,4 +1,4 @@
-const ipMatch = require("../../src/utils/ipMatch");
+const ipMatch = require("../../src/utils/ipMatch.js");
describe("IP address matching function", () => {
test("should return true if IP1 is empty", () => {
diff --git a/tests/utils/ipSubnetUtils.test.js b/tests/utils/ipSubnetUtils.test.js
index 340c8c9..41c3dea 100644
--- a/tests/utils/ipSubnetUtils.test.js
+++ b/tests/utils/ipSubnetUtils.test.js
@@ -1,7 +1,7 @@
const {
calculateBroadcastIPv4FromCidr,
calculateNetworkIPv4FromCidr,
-} = require("../../src/utils/ipSubnetUtils");
+} = require("../../src/utils/ipSubnetUtils.js");
describe("IPv4 subnet utilties", () => {
describe("calculateBroadcastIPv4FromCidr", () => {
diff --git a/tests/utils/matchHostname.test.js b/tests/utils/matchHostname.test.js
index 173f9ed..b826b40 100644
--- a/tests/utils/matchHostname.test.js
+++ b/tests/utils/matchHostname.test.js
@@ -1,4 +1,4 @@
-const matchHostname = require("../../src/utils/matchHostname");
+const matchHostname = require("../../src/utils/matchHostname.js");
describe("Hostname matching function", () => {
test("should return true if hostname is undefined", () => {
diff --git a/tests/utils/sha256.test.js b/tests/utils/sha256.test.js
index 543875b..13447bf 100644
--- a/tests/utils/sha256.test.js
+++ b/tests/utils/sha256.test.js
@@ -1,4 +1,4 @@
-const sha256 = require("../../src/utils/sha256");
+const sha256 = require("../../src/utils/sha256.js");
const crypto = require("crypto");
// Mock the crypto module to simulate the absence of crypto support
diff --git a/tests/utils/sizify.test.js b/tests/utils/sizify.test.js
index afafa57..404642a 100644
--- a/tests/utils/sizify.test.js
+++ b/tests/utils/sizify.test.js
@@ -1,4 +1,4 @@
-const sizify = require("../../src/utils/sizify");
+const sizify = require("../../src/utils/sizify.js");
describe('"sizify" function', () => {
test('should return "0" for 0 bytes', () => {