From 0450094c686019cc1e91dd887ac72598def309b4 Mon Sep 17 00:00:00 2001 From: Dorian Niemiec Date: Fri, 30 Aug 2024 22:11:16 +0200 Subject: [PATCH] Add tests for middleware, add ".js" file extensions inside the require functions in tests for utility functions, make error handling in URL rewriting middleware better, and lint out static file serving and directory listing middleware --- package-lock.json | 234 +++++++++++++++ package.json | 5 +- src/middleware/rewriteURL.js | 31 +- .../staticFileServingAndDirectoryListings.js | 4 +- tests/middleware/blocklist.test.js | 97 ++++++ tests/middleware/checkForbiddenPaths.test.js | 67 +++++ tests/middleware/defaultHandlerChecks.test.js | 54 ++++ ...StandardCodesAndHttpAuthentication.test.js | 281 ++++++++++++++++++ .../redirectTrailingSlashes.test.js | 91 ++++++ tests/middleware/redirects.test.js | 52 ++++ tests/middleware/responseHeaders.test.js | 48 +++ tests/middleware/rewriteURL.test.js | 143 +++++++++ ...aticFileServingAndDirectoryListing.test.js | 185 ++++++++++++ tests/middleware/status.test.js | 77 +++++ tests/middleware/urlSanitizer.test.js | 99 ++++++ tests/middleware/webRootPrefixes.test.js | 68 +++++ tests/utils/createRegex.test.js | 2 +- tests/utils/deepClone.test.js | 2 +- tests/utils/forbiddenPaths.test.js | 2 +- tests/utils/generateErrorStack.test.js | 2 +- tests/utils/ipBlockList.test.js | 2 +- tests/utils/ipMatch.test.js | 2 +- tests/utils/ipSubnetUtils.test.js | 2 +- tests/utils/matchHostname.test.js | 2 +- tests/utils/sha256.test.js | 2 +- tests/utils/sizify.test.js | 2 +- 26 files changed, 1527 insertions(+), 29 deletions(-) create mode 100644 tests/middleware/blocklist.test.js create mode 100644 tests/middleware/checkForbiddenPaths.test.js create mode 100644 tests/middleware/defaultHandlerChecks.test.js create mode 100644 tests/middleware/nonStandardCodesAndHttpAuthentication.test.js create mode 100644 tests/middleware/redirectTrailingSlashes.test.js create mode 100644 tests/middleware/redirects.test.js create mode 100644 tests/middleware/responseHeaders.test.js create mode 100644 tests/middleware/rewriteURL.test.js create mode 100644 tests/middleware/staticFileServingAndDirectoryListing.test.js create mode 100644 tests/middleware/status.test.js create mode 100644 tests/middleware/urlSanitizer.test.js create mode 100644 tests/middleware/webRootPrefixes.test.js 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', () => {