1
0
Fork 0
forked from svrjs/svrjs

Add web root postfixes middleware

This commit is contained in:
Dorian Niemiec 2024-08-24 08:02:11 +02:00
parent a7acd9e1f4
commit 1929641ba7
6 changed files with 346 additions and 1 deletions

View file

@ -85,7 +85,10 @@ if (configJSON.useWebRootServerSideScript === undefined) configJSON.useWebRootSe
if (configJSON.exposeModsInErrorPages === undefined) configJSON.exposeModsInErrorPages = true; if (configJSON.exposeModsInErrorPages === undefined) configJSON.exposeModsInErrorPages = true;
if (configJSON.disableTrailingSlashRedirects === undefined) configJSON.disableTrailingSlashRedirects = false; if (configJSON.disableTrailingSlashRedirects === undefined) configJSON.disableTrailingSlashRedirects = false;
if (configJSON.environmentVariables === undefined) configJSON.environmentVariables = {}; if (configJSON.environmentVariables === undefined) configJSON.environmentVariables = {};
if (configJSON.wwwrootPostfixesVHost === undefined) configJSON.wwwrootPostfixesVHost = [];
if (configJSON.wwwrootPostfixPrefixesVHost === undefined) configJSON.wwwrootPostfixPrefixesVHost = [];
if (configJSON.allowDoubleSlashes === undefined) configJSON.allowDoubleSlashes = false; if (configJSON.allowDoubleSlashes === undefined) configJSON.allowDoubleSlashes = false;
if (configJSON.allowPostfixDoubleSlashes === undefined) configJSON.allowPostfixDoubleSlashes = false;
if (configJSON.optOutOfStatisticsServer === undefined) configJSON.optOutOfStatisticsServer = false; if (configJSON.optOutOfStatisticsServer === undefined) configJSON.optOutOfStatisticsServer = false;
configJSON.version = version; // Compatiblity for very old SVR.JS mods configJSON.version = version; // Compatiblity for very old SVR.JS mods
@ -102,7 +105,8 @@ const serverconsole = serverconsoleConstructor(configJSON.enableLogging);
let middleware = [ let middleware = [
require("./middleware/core.js"), require("./middleware/core.js"),
require("./middleware/urlSanitizer.js"), require("./middleware/urlSanitizer.js"),
require("./middleware/redirects.js") require("./middleware/redirects.js"),
require("./middleware/webRootPostfixes.js")
]; ];
function addMiddleware(mw) { function addMiddleware(mw) {

View file

@ -0,0 +1,116 @@
const createRegex = require("../utils/createRegex.js");
const ipMatch = require("../utils/ipMatch.js");
const sanitizeURL = require("../utils/urlSanitizer.js");
module.exports = (req, res, logFacilities, config, next) => {
const matchHostname = (hostname) => {
if (typeof hostname == "undefined" || hostname == "*") {
return true;
} else if (
req.headers.host &&
hostname.indexOf("*.") == 0 &&
hostname != "*."
) {
const hostnamesRoot = hostname.substring(2);
if (
req.headers.host == hostnamesRoot ||
(req.headers.host.length > hostnamesRoot.length &&
req.headers.host.indexOf("." + hostnamesRoot) ==
req.headers.host.length - hostnamesRoot.length - 1)
) {
return true;
}
} else if (req.headers.host && req.headers.host == hostname) {
return true;
}
return false;
};
// Add web root postfixes
if (!req.isProxy) {
let preparedReqUrl3 = (config.allowPostfixDoubleSlashes ? (req.parsedURL.pathname.replace(/\/+/,"/") + req.parsedURL.search + req.parsedURL.hash) : req.url);
let urlWithPostfix = preparedReqUrl3;
let postfixPrefix = "";
config.wwwrootPostfixPrefixesVHost.every(function (currentPostfixPrefix) {
if (preparedReqUrl3.indexOf(currentPostfixPrefix) == 0) {
if (currentPostfixPrefix.match(/\/+$/)) postfixPrefix = currentPostfixPrefix.replace(/\/+$/, "");
else if (urlWithPostfix.length == currentPostfixPrefix.length || urlWithPostfix[currentPostfixPrefix.length] == "?" || urlWithPostfix[currentPostfixPrefix.length] == "/" || urlWithPostfix[currentPostfixPrefix.length] == "#") postfixPrefix = currentPostfixPrefix;
else return true;
urlWithPostfix = urlWithPostfix.substring(postfixPrefix.length);
return false;
} else {
return true;
}
});
config.wwwrootPostfixesVHost.every(function (postfixEntry) {
if (matchHostname(postfixEntry.host) && ipMatch(postfixEntry.ip, req.socket ? req.socket.localAddress : undefined) && !(postfixEntry.skipRegex && preparedReqUrl3.match(createRegex(postfixEntry.skipRegex)))) {
urlWithPostfix = postfixPrefix + "/" + postfixEntry.postfix + urlWithPostfix;
return false;
} else {
return true;
}
});
if (urlWithPostfix != preparedReqUrl3) {
logFacilities.resmessage("Added web root postfix: " + req.url + " => " + urlWithPostfix);
req.url = urlWithPostfix;
try {
req.parsedURL = new URL(
req.url,
"http" +
(req.socket.encrypted ? "s" : "") +
"://" +
(req.headers.host
? req.headers.host
: config.domain
? config.domain
: "unknown.invalid"),
);
} catch (err) {
res.error(400, err);
return;
}
let href = req.parsedURL.pathname + req.parsedURL.search;
var sHref = sanitizeURL(href, allowDoubleSlashes);
var preparedReqUrl2 = req.parsedURL.pathname + req.parsedURL.search + req.parsedURL.hash;
if (req.url != preparedReqUrl2 || sHref != href.replace(/\/\.(?=\/|$)/g, "/").replace(/\/+/g, "/")) {
res.error(403);
logFacilities.errmessage("Content blocked.");
return;
} else if (sHref != href) {
var rewrittenAgainURL = new url.Url();
rewrittenAgainURL.path = null;
rewrittenAgainURL.href = null;
rewrittenAgainURL.pathname = sHref;
rewrittenAgainURL.hostname = null;
rewrittenAgainURL.host = null;
rewrittenAgainURL.port = null;
rewrittenAgainURL.protocol = null;
rewrittenAgainURL.slashes = null;
rewrittenAgainURL = url.format(rewrittenAgainURL);
logFacilities.resmessage("URL sanitized: " + req.url + " => " + rewrittenAgainURL);
req.url = rewrittenAgainURL;
try {
req.parsedURL = new URL(
req.url,
"http" +
(req.socket.encrypted ? "s" : "") +
"://" +
(req.headers.host
? req.headers.host
: config.domain
? config.domain
: "unknown.invalid"),
);
} catch (err) {
res.error(400, err);
return;
}
}
}
}
next();
};

12
src/utils/createRegex.js Normal file
View file

@ -0,0 +1,12 @@
const os = require("os");
function createRegex(regex, isPath) {
var regexStrMatch = regex.match(/^\/((?:\\.|[^\/\\])*)\/([a-zA-Z0-9]*)$/);
if (!regexStrMatch) throw new Error("Invalid regular expression: " + regex);
var searchString = regexStrMatch[1];
var modifiers = regexStrMatch[2];
if (isPath && !modifiers.match(/i/i) && os.platform() == "win32") modifiers += "i";
return new RegExp(searchString, modifiers);
}
module.exports = createRegex;

78
src/utils/ipMatch.js Normal file
View file

@ -0,0 +1,78 @@
// Function to check if IPs are equal
function ipMatch(IP1, IP2) {
if (!IP1) return true;
if (!IP2) return false;
// Function to normalize IPv4 address (remove leading zeros)
function normalizeIPv4Address(address) {
return address.replace(/(^|\.)(?:0(?!\.|$))+/g, "$1");
}
// Function to expand IPv6 address to full format
function expandIPv6Address(address) {
var fullAddress = "";
var expandedAddress = "";
var validGroupCount = 8;
var validGroupSize = 4;
var ipv4 = "";
var extractIpv4 = /([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/;
var 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)) {
var oldGroups = address.match(extractIpv4);
for (var 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 {
var sides = address.split("::");
var groupsPresent = 0;
sides.forEach(function (side) {
groupsPresent += side.split(":").length;
});
fullAddress += sides[0] + ":";
if (validGroupCount - groupsPresent > 1) {
fullAddress += "0000:".repeat(validGroupCount - groupsPresent);
}
fullAddress += sides[1];
}
var groups = fullAddress.split(":");
for (var 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;
}
// Normalize or expand IP addresses
IP1 = IP1.toLowerCase();
if (IP1 == "localhost") IP1 = "::1";
if (IP1.indexOf("::ffff:") == 0) IP1 = IP1.substring(7);
if (IP1.indexOf(":") > -1) {
IP1 = expandIPv6Address(IP1);
} else {
IP1 = normalizeIPv4Address(IP1);
}
IP2 = IP2.toLowerCase();
if (IP2 == "localhost") IP2 = "::1";
if (IP2.indexOf("::ffff:") == 0) IP2 = IP2.substring(7);
if (IP2.indexOf(":") > -1) {
IP2 = expandIPv6Address(IP2);
} else {
IP2 = normalizeIPv4Address(IP2);
}
// Check if processed IPs are equal
if (IP1 == IP2) return true;
else return false;
}
module.exports = ipMatch;

View file

@ -0,0 +1,75 @@
const createRegex = require('../../src/utils/createRegex');
const os = require('os');
jest.mock('os', () => ({
platform: jest.fn(),
}));
describe('createRegex', () => {
beforeEach(() => {
os.platform.mockReset();
});
test('should throw an error for invalid regular expression', () => {
expect(() => createRegex('invalid/regex', false)).toThrow('Invalid regular expression: invalid/regex');
});
test('should create a regular expression without modifiers', () => {
const regex = createRegex('/test/', false);
expect(regex).toBeInstanceOf(RegExp);
expect(regex.source).toBe('test');
expect(regex.flags).toBe('');
});
test('should create a regular expression with modifiers', () => {
const regex = createRegex('/test/gi', false);
expect(regex).toBeInstanceOf(RegExp);
expect(regex.source).toBe('test');
expect(regex.flags).toBe('gi');
});
test('should add "i" modifier for paths on Windows', () => {
os.platform.mockReturnValue('win32');
const regex = createRegex('/test/', true);
expect(regex).toBeInstanceOf(RegExp);
expect(regex.source).toBe('test');
expect(regex.flags).toBe('i');
});
test('should not add "i" modifier for paths on non-Windows platforms', () => {
os.platform.mockReturnValue('linux');
const regex = createRegex('/test/', true);
expect(regex).toBeInstanceOf(RegExp);
expect(regex.source).toBe('test');
expect(regex.flags).toBe('');
});
test('should not add "i" modifier if already present', () => {
os.platform.mockReturnValue('win32');
const regex = createRegex('/test/i', true);
expect(regex).toBeInstanceOf(RegExp);
expect(regex.source).toBe('test');
expect(regex.flags).toBe('i');
});
test('should handle escaped characters in the search string', () => {
const regex = createRegex('/test\\/path/', false);
expect(regex).toBeInstanceOf(RegExp);
expect(regex.source).toBe('test\\/path');
expect(regex.flags).toBe('');
});
test('should handle multiple modifiers', () => {
const regex = createRegex('/test/gim', false);
expect(regex).toBeInstanceOf(RegExp);
expect(regex.source).toBe('test');
expect(regex.flags).toBe('gim');
});
test('should handle empty search string', () => {
const regex = createRegex('/^$/', false);
expect(regex).toBeInstanceOf(RegExp);
expect(regex.source).toBe('^$');
expect(regex.flags).toBe('');
});
});

View file

@ -0,0 +1,60 @@
const ipMatch = require('../../src/utils/ipMatch');
describe('ipMatch', () => {
test('should return true if IP1 is empty', () => {
expect(ipMatch('', '192.168.1.1')).toBe(true);
});
test('should return false if IP2 is empty', () => {
expect(ipMatch('192.168.1.1', '')).toBe(false);
});
test('should return true if both IPs are empty', () => {
expect(ipMatch('', '')).toBe(true);
});
test('should return true if both IPs are the same IPv4 address', () => {
expect(ipMatch('192.168.1.1', '192.168.1.1')).toBe(true);
});
test('should return false if IPs are different IPv4 addresses', () => {
expect(ipMatch('192.168.1.1', '192.168.1.2')).toBe(false);
});
test('should normalize IPv4 addresses with leading zeros', () => {
expect(ipMatch('192.168.001.001', '192.168.1.1')).toBe(true);
});
test('should return true if both IPs are the same IPv6 address', () => {
expect(ipMatch('2001:0db8:85a3:0000:0000:8a2e:0370:7334', '2001:db8:85a3::8a2e:370:7334')).toBe(true);
});
test('should return false if IPs are different IPv6 addresses', () => {
expect(ipMatch('2001:0db8:85a3:0000:0000:8a2e:0370:7334', '2001:db8:85a3::8a2e:370:7335')).toBe(false);
});
test('should expand IPv6 addresses with "::"', () => {
expect(ipMatch('::1', '0:0:0:0:0:0:0:1')).toBe(true);
});
test('should handle IPv6 addresses with embedded IPv4 addresses', () => {
expect(ipMatch('::ffff:192.168.1.1', '192.168.1.1')).toBe(true);
});
test('should handle "localhost" as IPv6 loopback address', () => {
expect(ipMatch('localhost', '::1')).toBe(true);
});
test('should handle mixed case IP addresses', () => {
expect(ipMatch('192.168.1.1', '192.168.1.1')).toBe(true);
expect(ipMatch('2001:DB8:85A3::8A2E:370:7334', '2001:db8:85a3::8a2e:370:7334')).toBe(true);
});
test('should handle IPv6 addresses with leading zeros', () => {
expect(ipMatch('2001:0db8:85a3:0000:0000:8a2e:0370:7334', '2001:db8:85a3::8a2e:370:7334')).toBe(true);
});
test('should handle IPv6 addresses with mixed case and leading zeros', () => {
expect(ipMatch('2001:0DB8:85A3:0000:0000:8A2E:0370:7334', '2001:db8:85a3::8a2e:370:7334')).toBe(true);
});
});