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', () => {