diff --git a/src/index.js b/src/index.js index 50fd45e..bb851b4 100644 --- a/src/index.js +++ b/src/index.js @@ -94,6 +94,21 @@ const cluster = require("./utils/clusterBunShim.js"); // Cluster module with shi //const fixNodeMojibakeURL = require("./utils/urlMojibakeFixer.js"); process.serverConfig = {}; +let configJSONRErr = undefined; +let configJSONPErr = undefined; +if (fs.existsSync(__dirname + "/config.json")) { + let configJSONf = ""; + try { + configJSONf = fs.readFileSync(__dirname + "/config.json"); // Read JSON File + try { + process.serverConfig = JSON.parse(configJSONf); // Parse JSON + } catch (err2) { + configJSONPErr = err2; + } + } catch (err) { + configJSONRErr = err2; + } +} // TODO: configuration from config.json if (process.serverConfig.users === undefined) process.serverConfig.users = []; @@ -115,7 +130,6 @@ delete process.serverConfig.domian; if (process.serverConfig.page404 === undefined) process.serverConfig.page404 = "404.html"; process.serverConfig.timestamp = new Date().getTime(); if (process.serverConfig.blacklist === undefined) process.serverConfig.blacklist = []; -//process.serverConfig.blacklist = blocklist.raw; //TODO if (process.serverConfig.nonStandardCodes === undefined) process.serverConfig.nonStandardCodes = []; if (process.serverConfig.enableCompression === undefined) process.serverConfig.enableCompression = true; if (process.serverConfig.customHeaders === undefined) process.serverConfig.customHeaders = {}; @@ -182,7 +196,7 @@ let middleware = [ require("./middleware/core.js"), require("./middleware/urlSanitizer.js"), require("./middleware/redirects.js"), - // TODO: blocklist + require("./middleware/blocklist.js"), require("./middleware/webRootPostfixes.js"), require("./middleware/rewriteURL.js"), require("./middleware/responseHeaders.js"), @@ -254,4 +268,6 @@ function requestHandler(req, res) { // Create HTTP server http.createServer(requestHandler).listen(3000); -if(wwwrootError) throw wwwrootError; \ No newline at end of file +if(wwwrootError) throw wwwrootError; +if(configJSONRErr) throw configJSONRErr; +if(configJSONPErr) throw configJSONPErr; \ No newline at end of file diff --git a/src/middleware/blocklist.js b/src/middleware/blocklist.js new file mode 100644 index 0000000..1b49353 --- /dev/null +++ b/src/middleware/blocklist.js @@ -0,0 +1,53 @@ +const ipBlockList = require("../utils/ipBlockList.js"); +let blocklist = ipBlockList(process.serverConfig.blacklist); + +module.exports = (req, res, logFacilities, config, next) => { + if ( + blocklist.check( + req.socket.realRemoteAddress + ? req.socket.realRemoteAddress + : req.socket.remoteAddress, + ) + ) { + // Invoke 403 Forbidden error + res.error(403); + logFacilities.errmessage("Client is in the block list."); + return; + } + next(); +}; + +module.exports.commands = { + block: (ip, logFacilities, passCommand) => { + if (ip == undefined || JSON.stringify(ip) == "[]") { + log("Cannot block non-existent IP."); + } else { + for (var i = 0; i < ip.length; i++) { + if (ip[i] != "localhost" && ip[i].indexOf(":") == -1) { + ip[i] = "::ffff:" + ip[i]; + } + if (!blocklist.check(ip[i])) { + blocklist.add(ip[i]); + } + } + process.config.blacklist = blocklist.raw; + log("IPs successfully blocked."); + passCommand(args, logFacilities); + } + }, + unblock: (ip, logFacilities, passCommand) => { + if (ip == undefined || JSON.stringify(ip) == "[]") { + log("Cannot unblock non-existent IP."); + } else { + for (var i = 0; i < ip.length; i++) { + if (ip[i].indexOf(":") == -1) { + ip[i] = "::ffff:" + ip[i]; + } + blocklist.remove(ip[i]); + } + process.config.blacklist = blocklist.raw; + log("IPs successfully unblocked."); + passCommand(args, logFacilities); + } + }, +}; diff --git a/src/middleware/nonStandardCodesAndHttpAuthentication.js b/src/middleware/nonStandardCodesAndHttpAuthentication.js index 16bc0a5..e570895 100644 --- a/src/middleware/nonStandardCodesAndHttpAuthentication.js +++ b/src/middleware/nonStandardCodesAndHttpAuthentication.js @@ -13,6 +13,20 @@ let pbkdf2Cache = []; let scryptCache = []; let passwordHashCacheIntervalId = -1; +// Non-standard code object +let nonStandardCodes = []; +process.serverConfig.nonStandardCodes.forEach((nonStandardCodeRaw) => { + var newObject = {}; + Object.keys(nonStandardCodeRaw).forEach((nsKey) => { + if (nsKey != "users") { + newObject[nsKey] = nonStandardCodeRaw[nsKey]; + } else { + newObject["users"] = ipBlockList(nonStandardCodeRaw.users); + } + }); + nonStandardCodes.push(newObject); +}); + if (!cluster.isPrimary) { passwordHashCacheIntervalId = setInterval(function () { pbkdf2Cache = pbkdf2Cache.filter(function (entry) { @@ -34,12 +48,12 @@ module.exports = (req, res, logFacilities, config, next) => { : req.socket.remoteAddress; // Scan for non-standard codes - if (!req.isProxy && config.nonStandardCodes != undefined) { - for (let i = 0; i < config.nonStandardCodes.length; i++) { + if (!req.isProxy && nonStandardCodes != undefined) { + for (let i = 0; i < nonStandardCodes.length; i++) { if ( - matchHostname(config.nonStandardCodes[i].host, req.headers.host) && + matchHostname(nonStandardCodes[i].host, req.headers.host) && ipMatch( - config.nonStandardCodes[i].ip, + nonStandardCodes[i].ip, req.socket ? req.socket.localAddress : undefined, ) ) { @@ -48,12 +62,9 @@ module.exports = (req, res, logFacilities, config, next) => { /\/+/g, "/", ); - if (config.nonStandardCodes[i].regex) { + if (nonStandardCodes[i].regex) { // Regex match - var createdRegex = createRegex( - config.nonStandardCodes[i].regex, - true, - ); + var createdRegex = createRegex(nonStandardCodes[i].regex, true); isMatch = req.url.match(createdRegex) || hrefWithoutDuplicateSlashes.match(createdRegex); @@ -61,13 +72,13 @@ module.exports = (req, res, logFacilities, config, next) => { } else { // Non-regex match isMatch = - config.nonStandardCodes[i].url == hrefWithoutDuplicateSlashes || + nonStandardCodes[i].url == hrefWithoutDuplicateSlashes || (os.platform() == "win32" && - config.nonStandardCodes[i].url.toLowerCase() == + nonStandardCodes[i].url.toLowerCase() == hrefWithoutDuplicateSlashes.toLowerCase()); } if (isMatch) { - if (config.nonStandardCodes[i].scode == 401) { + if (nonStandardCodes[i].scode == 401) { // HTTP authentication if (authIndex == -1) { authIndex = i; @@ -75,12 +86,11 @@ module.exports = (req, res, logFacilities, config, next) => { } else { if (nonscodeIndex == -1) { if ( - (config.nonStandardCodes[i].scode == 403 || - config.nonStandardCodes[i].scode == 451) && - config.nonStandardCodes[i].users !== undefined + (nonStandardCodes[i].scode == 403 || + nonStandardCodes[i].scode == 451) && + nonStandardCodes[i].users !== undefined ) { - if (config.nonStandardCodes[i].users.check(reqip)) - nonscodeIndex = i; + if (nonStandardCodes[i].users.check(reqip)) nonscodeIndex = i; } else { nonscodeIndex = i; } @@ -93,7 +103,7 @@ module.exports = (req, res, logFacilities, config, next) => { // Handle non-standard codes if (nonscodeIndex > -1) { - let nonscode = config.nonStandardCodes[nonscodeIndex]; + let nonscode = nonStandardCodes[nonscodeIndex]; if ( nonscode.scode == 301 || nonscode.scode == 302 || @@ -143,7 +153,7 @@ module.exports = (req, res, logFacilities, config, next) => { // Handle HTTP authentication if (authIndex > -1) { - let authcode = config.nonStandardCodes[authIndex]; + let authcode = nonStandardCodes[authIndex]; // Function to check if passwords match const checkIfPasswordMatches = (list, password, callback, _i) => { @@ -446,8 +456,8 @@ module.exports.mainMessageListenerWrapper = (worker) => { }; module.exports.commands = { - stop: (args, passCommand) => { + stop: (args, log, passCommand) => { clearInterval(passwordHashCacheIntervalId); - passCommand(args); + passCommand(args, log); }, }; diff --git a/src/utils/ipBlockList.js b/src/utils/ipBlockList.js new file mode 100644 index 0000000..2d4fda7 --- /dev/null +++ b/src/utils/ipBlockList.js @@ -0,0 +1,252 @@ +// IP Block list object +function ipBlockList(rawBlockList) { + // Initialize the instance with empty arrays + if (rawBlockList === undefined) rawBlockList = []; + var instance = { + raw: [], + rawtoPreparedMap: [], + prepared: [], + cidrs: [], + }; + + // Function to normalize IPv4 address (remove leading zeros) + const normalizeIPv4Address = (address) => { + return address.replace(/(^|\.)(?:0(?!\.|$))+/g, "$1"); + }; + + // Function to expand IPv6 address to full format + const expandIPv6Address = (address) => { + let fullAddress = ""; + let expandedAddress = ""; + let validGroupCount = 8; + let validGroupSize = 4; + + let ipv4 = ""; + const extractIpv4 = + /([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/; + const validateIpv4 = + /((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})/; + + if (validateIpv4.test(address)) { + const oldGroups = address.match(extractIpv4); + for (let i = 1; i < oldGroups.length; i++) { + ipv4 += + ("00" + parseInt(oldGroups[i], 10).toString(16)).slice(-2) + + (i == 2 ? ":" : ""); + } + address = address.replace(extractIpv4, ipv4); + } + + if (address.indexOf("::") == -1) { + fullAddress = address; + } else { + const sides = address.split("::"); + let groupsPresent = 0; + sides.forEach((side) => { + groupsPresent += side.split(":").length; + }); + fullAddress += sides[0] + ":"; + if (validGroupCount - groupsPresent > 1) { + fullAddress += "0000:".repeat(validGroupCount - groupsPresent); + } + fullAddress += sides[1]; + } + let groups = fullAddress.split(":"); + for (let i = 0; i < validGroupCount; i++) { + if (groups[i].length < validGroupSize) { + groups[i] = "0".repeat(validGroupSize - groups[i].length) + groups[i]; + } + expandedAddress += i != validGroupCount - 1 ? groups[i] + ":" : groups[i]; + } + return expandedAddress; + }; + + // Convert IPv4 address to an integer representation + const ipv4ToInt = (ip) => { + const ips = ip.split("."); + return ( + parseInt(ips[0]) * 16777216 + + parseInt(ips[1]) * 65536 + + parseInt(ips[2]) * 256 + + parseInt(ips[3]) + ); + }; + + // Get IPv4 CIDR block limits (min and max) + const getIPv4CIDRLimits = (ip, cidrMask) => { + const ipInt = ipv4ToInt(ip); + const exp = Math.pow(2, 32 - cidrMask); + const ipMin = Math.floor(ipInt / exp) * exp; + const ipMax = ipMin + exp - 1; + return { + min: ipMin, + max: ipMax, + }; + }; + + // Convert IPv6 address to an array of blocks + const ipv6ToBlocks = (ip) => { + const ips = ip.split(":"); + let ip2s = []; + ips.forEach((ipe) => { + ip2s.push(parseInt(ipe, 16)); + }); + return ip2s; + }; + + // Get IPv6 CIDR block limits (min and max) + const getIPv6CIDRLimits = (ip, cidrMask) => { + const ipBlocks = ipv6ToBlocks(ip); + const fieldsToDelete = Math.floor((128 - cidrMask) / 16); + const fieldMaskModify = (128 - cidrMask) % 16; + let ipBlockMin = []; + let ipBlockMax = []; + for (let i = 0; i < 8; i++) { + ipBlockMin.push( + i < 8 - fieldsToDelete + ? i < 7 - fieldsToDelete + ? ipBlocks[i] + : (ipBlocks[i] >> fieldMaskModify) << fieldMaskModify + : 0, + ); + } + for (let i = 0; i < 8; i++) { + ipBlockMax.push( + i < 8 - fieldsToDelete + ? i < 7 - fieldsToDelete + ? ipBlocks[i] + : ((ipBlocks[i] >> fieldMaskModify) << fieldMaskModify) + + Math.pow(2, fieldMaskModify) - + 1 + : 65535, + ); + } + return { + min: ipBlockMin, + max: ipBlockMax, + }; + }; + + // Check if the IPv4 address matches the given CIDR block + const checkIfIPv4CIDRMatches = (ipInt, cidrObject) => { + if (cidrObject.v6) return false; + return ipInt >= cidrObject.min && ipInt <= cidrObject.max; + }; + + // Check if the IPv6 address matches the given CIDR block + const checkIfIPv6CIDRMatches = (ipBlock, cidrObject) => { + if (!cidrObject.v6) return false; + for (let i = 0; i < 8; i++) { + if (ipBlock[i] < cidrObject.min[i] || ipBlock[i] > cidrObject.max[i]) + return false; + } + return true; + }; + + // Function to add an IP or CIDR block to the block list + instance.add = (rawValue) => { + // Add to raw block list + instance.raw.push(rawValue); + + // Initialize variables + const beginIndex = instance.prepared.length; + const cidrIndex = instance.cidrs.length; + let cidrMask = null; + let isIPv6 = false; + + // Check if the input contains CIDR notation + if (rawValue.indexOf("/") > -1) { + const rwArray = rawValue.split("/"); + cidrMask = rwArray.pop(); + rawValue = rwArray.join("/"); + } + + // Normalize the IP address or expand the IPv6 address + rawValue = rawValue.toLowerCase(); + if (rawValue.indexOf("::ffff:") == 0) rawValue = rawValue.substring(7); + if (rawValue.indexOf(":") > -1) { + isIPv6 = true; + rawValue = expandIPv6Address(rawValue); + } else { + rawValue = normalizeIPv4Address(rawValue); + } + + // Add the IP or CIDR block to the appropriate list + if (cidrMask) { + let cidrLimits = {}; + if (isIPv6) { + cidrLimits = getIPv6CIDRLimits(rawValue, cidrMask); + cidrLimits.v6 = true; + } else { + cidrLimits = getIPv4CIDRLimits(rawValue, cidrMask); + cidrLimits.v6 = false; + } + instance.cidrs.push(cidrLimits); + instance.rawtoPreparedMap.push({ + cidr: true, + index: cidrIndex, + }); + } else { + instance.prepared.push(rawValue); + instance.rawtoPreparedMap.push({ + cidr: false, + index: beginIndex, + }); + } + }; + + // Function to remove an IP or CIDR block from the block list + instance.remove = (ip) => { + const index = instance.raw.indexOf(ip); + if (index == -1) return false; + const map = instance.rawtoPreparedMap[index]; + instance.raw.splice(index, 1); + instance.rawtoPreparedMap.splice(index, 1); + if (map.cidr) { + instance.cidrs.splice(map.index, 1); + } else { + instance.prepared.splice(map.index, 1); + } + return true; + }; + + // Function to check if an IP is blocked by the block list + instance.check = (rawValue) => { + if (instance.raw.length == 0) return false; + let isIPv6 = false; + + // Normalize or expand the IP address + rawValue = rawValue.toLowerCase(); + if (rawValue == "localhost") rawValue = "::1"; + if (rawValue.indexOf("::ffff:") == 0) rawValue = rawValue.substring(7); + if (rawValue.indexOf(":") > -1) { + isIPv6 = true; + rawValue = expandIPv6Address(rawValue); + } else { + rawValue = normalizeIPv4Address(rawValue); + } + + // Check if the IP is in the prepared list + if (instance.prepared.indexOf(rawValue) > -1) return true; + + // Check if the IP is within any CIDR block in the block list + if (instance.cidrs.length == 0) return false; + const ipParsedObject = (!isIPv6 ? ipv4ToInt : ipv6ToBlocks)(rawValue); + const checkMethod = !isIPv6 + ? checkIfIPv4CIDRMatches + : checkIfIPv6CIDRMatches; + + return instance.cidrs.some((iCidr) => { + return checkMethod(ipParsedObject, iCidr); + }); + }; + + // Add initial raw block list values to the instance + rawBlockList.forEach((rbe) => { + instance.add(rbe); + }); + + return instance; +} + +module.exports = ipBlockList; diff --git a/src/utils/ipMatch.js b/src/utils/ipMatch.js index a5e2120..1370ff3 100644 --- a/src/utils/ipMatch.js +++ b/src/utils/ipMatch.js @@ -4,12 +4,12 @@ function ipMatch(IP1, IP2) { if (!IP2) return false; // Function to normalize IPv4 address (remove leading zeros) - function normalizeIPv4Address(address) { + const normalizeIPv4Address = (address) => { return address.replace(/(^|\.)(?:0(?!\.|$))+/g, "$1"); - } + }; // Function to expand IPv6 address to full format - function expandIPv6Address(address) { + const expandIPv6Address = (address) => { let fullAddress = ""; let expandedAddress = ""; let validGroupCount = 8; @@ -53,7 +53,7 @@ function ipMatch(IP1, IP2) { expandedAddress += i != validGroupCount - 1 ? groups[i] + ":" : groups[i]; } return expandedAddress; - } + }; // Normalize or expand IP addresses IP1 = IP1.toLowerCase(); diff --git a/tests/utils/ipBlockList.test.js b/tests/utils/ipBlockList.test.js new file mode 100644 index 0000000..111310b --- /dev/null +++ b/tests/utils/ipBlockList.test.js @@ -0,0 +1,81 @@ +const ipBlockList = require("../../src/utils/ipBlockList"); + +describe("IP block list functionality", () => { + let blockList; + + beforeEach(() => { + blockList = ipBlockList([]); + }); + + test("should add and check IPv4 address", () => { + blockList.add("192.168.1.1"); + expect(blockList.check("192.168.1.1")).toBe(true); + expect(blockList.check("192.168.1.2")).toBe(false); + }); + + test("should add and check IPv6 address", () => { + blockList.add("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); + expect(blockList.check("2001:0db8:85a3:0000:0000:8a2e:0370:7334")).toBe( + true, + ); + expect(blockList.check("2001:0db8:85a3:0000:0000:8a2e:0370:7335")).toBe( + false, + ); + }); + + test("should add and check IPv4 CIDR block", () => { + blockList.add("192.168.1.0/24"); + expect(blockList.check("192.168.1.1")).toBe(true); + expect(blockList.check("192.168.1.255")).toBe(true); + expect(blockList.check("192.168.2.1")).toBe(false); + }); + + test("should add and check IPv6 CIDR block", () => { + blockList.add("2001:0db8:85a3::/64"); + expect(blockList.check("2001:0db8:85a3:0000:0000:8a2e:0370:7334")).toBe( + true, + ); + expect(blockList.check("2001:0db8:85a3:0000:0000:8a2e:0370:7335")).toBe( + true, + ); + expect(blockList.check("2001:0db8:85a4:0000:0000:8a2e:0370:7334")).toBe( + false, + ); + }); + + test("should remove IPv4 address", () => { + blockList.add("192.168.1.1"); + expect(blockList.check("192.168.1.1")).toBe(true); + blockList.remove("192.168.1.1"); + expect(blockList.check("192.168.1.1")).toBe(false); + }); + + test("should remove IPv6 address", () => { + blockList.add("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); + expect(blockList.check("2001:0db8:85a3:0000:0000:8a2e:0370:7334")).toBe( + true, + ); + blockList.remove("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); + expect(blockList.check("2001:0db8:85a3:0000:0000:8a2e:0370:7334")).toBe( + false, + ); + }); + + test("should remove IPv4 CIDR block", () => { + blockList.add("192.168.1.0/24"); + expect(blockList.check("192.168.1.1")).toBe(true); + blockList.remove("192.168.1.0/24"); + expect(blockList.check("192.168.1.1")).toBe(false); + }); + + test("should remove IPv6 CIDR block", () => { + blockList.add("2001:0db8:85a3::/64"); + expect(blockList.check("2001:0db8:85a3:0000:0000:8a2e:0370:7334")).toBe( + true, + ); + blockList.remove("2001:0db8:85a3::/64"); + expect(blockList.check("2001:0db8:85a3:0000:0000:8a2e:0370:7334")).toBe( + false, + ); + }); +});