forked from svrjs/svrjs
Add web root postfixes middleware
This commit is contained in:
parent
a7acd9e1f4
commit
1929641ba7
6 changed files with 346 additions and 1 deletions
|
@ -85,7 +85,10 @@ if (configJSON.useWebRootServerSideScript === undefined) configJSON.useWebRootSe
|
|||
if (configJSON.exposeModsInErrorPages === undefined) configJSON.exposeModsInErrorPages = true;
|
||||
if (configJSON.disableTrailingSlashRedirects === undefined) configJSON.disableTrailingSlashRedirects = false;
|
||||
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.allowPostfixDoubleSlashes === undefined) configJSON.allowPostfixDoubleSlashes = false;
|
||||
if (configJSON.optOutOfStatisticsServer === undefined) configJSON.optOutOfStatisticsServer = false;
|
||||
|
||||
configJSON.version = version; // Compatiblity for very old SVR.JS mods
|
||||
|
@ -102,7 +105,8 @@ const serverconsole = serverconsoleConstructor(configJSON.enableLogging);
|
|||
let middleware = [
|
||||
require("./middleware/core.js"),
|
||||
require("./middleware/urlSanitizer.js"),
|
||||
require("./middleware/redirects.js")
|
||||
require("./middleware/redirects.js"),
|
||||
require("./middleware/webRootPostfixes.js")
|
||||
];
|
||||
|
||||
function addMiddleware(mw) {
|
||||
|
|
116
src/middleware/webRootPostfixes.js
Normal file
116
src/middleware/webRootPostfixes.js
Normal 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
12
src/utils/createRegex.js
Normal 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
78
src/utils/ipMatch.js
Normal 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;
|
75
tests/utils/createRegex.test.js
Normal file
75
tests/utils/createRegex.test.js
Normal 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('');
|
||||
});
|
||||
});
|
60
tests/utils/ipMatch.test.js
Normal file
60
tests/utils/ipMatch.test.js
Normal 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);
|
||||
});
|
||||
});
|
Reference in a new issue