1
0
Fork 0
forked from svrjs/svrjs

Add ipBlockList utility function, blocklist functionality, and read from config.json functionality.

This commit is contained in:
Dorian Niemiec 2024-08-25 09:12:39 +02:00
parent 2f836231f4
commit 89e9b35829
6 changed files with 440 additions and 28 deletions

View file

@ -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"),
@ -255,3 +269,5 @@ function requestHandler(req, res) {
http.createServer(requestHandler).listen(3000);
if(wwwrootError) throw wwwrootError;
if(configJSONRErr) throw configJSONRErr;
if(configJSONPErr) throw configJSONPErr;

View file

@ -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);
}
},
};

View file

@ -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);
},
};

252
src/utils/ipBlockList.js Normal file
View file

@ -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;

View file

@ -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();

View file

@ -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,
);
});
});