Add non-standard codes and HTTP authentication middleware, and SHA256 utility function.

This commit is contained in:
Dorian Niemiec 2024-08-24 20:32:06 +02:00
parent eb0d30f307
commit c1900ee128
4 changed files with 750 additions and 1 deletions

View file

@ -184,7 +184,8 @@ let middleware = [
require("./middleware/webRootPostfixes.js"), require("./middleware/webRootPostfixes.js"),
require("./middleware/rewriteURL.js"), require("./middleware/rewriteURL.js"),
require("./middleware/responseHeaders.js"), require("./middleware/responseHeaders.js"),
require("./middleware/checkForbiddenPaths.js") require("./middleware/checkForbiddenPaths.js"),
require("./middleware/nonStandardCodesAndHttpAuthentication.js")
]; ];
function addMiddleware(mw) { function addMiddleware(mw) {

View file

@ -0,0 +1,453 @@
const os = require("os");
const sha256 = require("../utils/sha256.js");
const createRegex = require("../utils/createRegex.js");
const ipMatch = require("../utils/ipMatch.js");
const matchHostname = require("../utils/matchHostname.js");
const cluster = require("../utils/clusterBunShim.js");
// Brute force protection-related
let bruteForceDb = {};
// PBKDF2/scrypt cache
let pbkdf2Cache = [];
let scryptCache = [];
let passwordHashCacheIntervalId = -1;
if (!cluster.isPrimary) {
passwordHashCacheIntervalId = setInterval(function () {
pbkdf2Cache = pbkdf2Cache.filter(function (entry) {
return entry.addDate > new Date() - 3600000;
});
scryptCache = scryptCache.filter(function (entry) {
return entry.addDate > new Date() - 3600000;
});
}, 1800000);
}
module.exports = (req, res, logFacilities, config, next) => {
let nonscodeIndex = -1;
let authIndex = -1;
let regexI = [];
let hrefWithoutDuplicateSlashes = "";
const reqip = req.socket.realRemoteAddress
? req.socket.realRemoteAddress
: req.socket.remoteAddress;
// Scan for non-standard codes
if (!req.isProxy && config.nonStandardCodes != undefined) {
for (let i = 0; i < config.nonStandardCodes.length; i++) {
if (
matchHostname(config.nonStandardCodes[i].host, req.headers.host) &&
ipMatch(
config.nonStandardCodes[i].ip,
req.socket ? req.socket.localAddress : undefined,
)
) {
let isMatch = false;
hrefWithoutDuplicateSlashes = req.parsedURL.pathname.replace(
/\/+/g,
"/",
);
if (config.nonStandardCodes[i].regex) {
// Regex match
var createdRegex = createRegex(
config.nonStandardCodes[i].regex,
true,
);
isMatch =
req.url.match(createdRegex) ||
hrefWithoutDuplicateSlashes.match(createdRegex);
regexI[i] = createdRegex;
} else {
// Non-regex match
isMatch =
config.nonStandardCodes[i].url == hrefWithoutDuplicateSlashes ||
(os.platform() == "win32" &&
config.nonStandardCodes[i].url.toLowerCase() ==
hrefWithoutDuplicateSlashes.toLowerCase());
}
if (isMatch) {
if (config.nonStandardCodes[i].scode == 401) {
// HTTP authentication
if (authIndex == -1) {
authIndex = i;
}
} else {
if (nonscodeIndex == -1) {
if (
(config.nonStandardCodes[i].scode == 403 ||
config.nonStandardCodes[i].scode == 451) &&
config.nonStandardCodes[i].users !== undefined
) {
if (config.nonStandardCodes[i].users.check(reqip))
nonscodeIndex = i;
} else {
nonscodeIndex = i;
}
}
}
}
}
}
}
// Handle non-standard codes
if (nonscodeIndex > -1) {
let nonscode = config.nonStandardCodes[nonscodeIndex];
if (
nonscode.scode == 301 ||
nonscode.scode == 302 ||
nonscode.scode == 307 ||
nonscode.scode == 308
) {
let location = "";
if (regexI[nonscodeIndex]) {
location = req.url.replace(regexI[nonscodeIndex], nonscode.location);
if (location == req.url) {
// Fallback replacement
location = hrefWithoutDuplicateSlashes.replace(
regexI[nonscodeIndex],
nonscode.location,
);
}
} else if (
req.url.split("?")[1] == undefined ||
req.url.split("?")[1] == null ||
req.url.split("?")[1] == "" ||
req.url.split("?")[1] == " "
) {
location = nonscode.location;
} else {
location = nonscode.location + "?" + req.url.split("?")[1];
}
res.redirect(
location,
nonscode.scode == 302 || nonscode.scode == 307,
nonscode.scode == 307 || nonscode.scode == 308,
);
return;
} else {
res.error(nonscode.scode);
if (nonscode.scode == 403) {
logFacilities.errmessage("Content blocked.");
} else if (nonscode.scode == 410) {
logFacilities.errmessage("Content is gone.");
} else if (nonscode.scode == 418) {
logFacilities.errmessage("SVR.JS is always a teapot ;)");
} else {
logFacilities.errmessage("Client fails receiving content.");
}
return;
}
}
// Handle HTTP authentication
if (authIndex > -1) {
let authcode = config.nonStandardCodes[authIndex];
// Function to check if passwords match
const checkIfPasswordMatches = (list, password, callback, _i) => {
if (!_i) _i = 0;
const cb = function (hash) {
if (hash == list[_i].pass) {
callback(true);
} else if (_i >= list.length - 1) {
callback(false);
} else {
checkIfPasswordMatches(list, password, callback, _i + 1);
}
};
let hashedPassword = sha256(password + list[_i].salt);
let cacheEntry = null;
if (list[_i].scrypt) {
if (!crypto.scrypt) {
res.error(
500,
new Error(
"SVR.JS doesn't support scrypt-hashed passwords on Node.JS versions without scrypt hash support.",
),
);
return;
} else {
cacheEntry = scryptCache.find(function (entry) {
return (
entry.password == hashedPassword && entry.salt == list[_i].salt
);
});
if (cacheEntry) {
cb(cacheEntry.hash);
} else {
crypto.scrypt(
password,
list[_i].salt,
64,
function (err, derivedKey) {
if (err) {
res.error(500, err);
} else {
const key = derivedKey.toString("hex");
scryptCache.push({
hash: key,
password: hashedPassword,
salt: list[_i].salt,
addDate: new Date(),
});
cb(key);
}
},
);
}
}
} else if (list[_i].pbkdf2) {
if (crypto.__disabled__ !== undefined) {
res.error(
500,
new Error(
"SVR.JS doesn't support PBKDF2-hashed passwords on Node.JS versions without crypto support.",
),
);
return;
} else {
cacheEntry = pbkdf2Cache.find(function (entry) {
return (
entry.password == hashedPassword && entry.salt == list[_i].salt
);
});
if (cacheEntry) {
cb(cacheEntry.hash);
} else {
crypto.pbkdf2(
password,
list[_i].salt,
36250,
64,
"sha512",
function (err, derivedKey) {
if (err) {
res.error(500, err);
} else {
const key = derivedKey.toString("hex");
pbkdf2Cache.push({
hash: key,
password: hashedPassword,
salt: list[_i].salt,
addDate: new Date(),
});
cb(key);
}
},
);
}
}
} else {
cb(hashedPassword);
}
};
const authorizedCallback = (bruteProtection) => {
try {
const ha = config.getCustomHeaders();
ha["WWW-Authenticate"] =
'Basic realm="' +
(authcode.realm
? authcode.realm.replace(/(\\|")/g, "\\$1")
: "SVR.JS HTTP Basic Authorization") +
'", charset="UTF-8"';
const credentials = req.headers["authorization"];
if (!credentials) {
res.error(401, ha);
logFacilities.errmessage("Content needs authorization.");
return;
}
const credentialsMatch = credentials.match(/^Basic (.+)$/);
if (!credentialsMatch) {
res.error(401, ha);
logFacilities.errmessage("Malformed credentials.");
return;
}
const decodedCredentials = Buffer.from(
credentialsMatch[1],
"base64",
).toString("utf8");
const decodedCredentialsMatch =
decodedCredentials.match(/^([^:]*):(.*)$/);
if (!decodedCredentialsMatch) {
res.error(401, ha);
logFacilities.errmessage("Malformed credentials.");
return;
}
const username = decodedCredentialsMatch[1];
const password = decodedCredentialsMatch[2];
let usernameMatch = [];
let sha256Count = 0;
let pbkdf2Count = 0;
let scryptCount = 0;
if (!authcode.userList || authcode.userList.indexOf(username) > -1) {
usernameMatch = config.users.filter(function (entry) {
if (entry.scrypt) {
scryptCount++;
} else if (entry.pbkdf2) {
pbkdf2Count++;
} else {
sha256Count++;
}
return entry.name == username;
});
}
if (usernameMatch.length == 0) {
// Pushing false user match to prevent time-based user enumeration
let fakeCredentials = {
name: username,
pass: "SVRJSAWebServerRunningOnNodeJS",
salt: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0",
};
if (!process.isBun) {
if (scryptCount > sha256Count && scryptCount > pbkdf2Count) {
fakeCredentials.scrypt = true;
} else if (pbkdf2Count > sha256Count) {
fakeCredentials.pbkdf2 = true;
}
}
usernameMatch.push(fakeCredentials);
}
checkIfPasswordMatches(usernameMatch, password, function (authorized) {
try {
if (!authorized) {
if (bruteProtection) {
if (process.send) {
process.send("\x12AUTHW" + reqip);
} else {
if (!bruteForceDb[reqip])
bruteForceDb[reqip] = {
invalidAttempts: 0,
};
bruteForceDb[reqip].invalidAttempts++;
if (bruteForceDb[reqip].invalidAttempts >= 10) {
bruteForceDb[reqip].lastAttemptDate = new Date();
}
}
}
res.error(401, ha);
logFacilities.errmessage(
'User "' +
String(username).replace(/[\r\n]/g, "") +
'" failed to log in.',
);
} else {
if (bruteProtection) {
if (process.send) {
process.send("\x12AUTHR" + reqip);
} else {
if (bruteForceDb[reqip])
bruteForceDb[reqip] = {
invalidAttempts: 0,
};
}
}
logFacilities.reqmessage(
'Client is logged in as "' +
String(username).replace(/[\r\n]/g, "") +
'".',
);
req.authUser = username;
next();
}
} catch (err) {
res.error(500, err);
return;
}
});
} catch (err) {
res.error(500, err);
return;
}
};
if (authcode.disableBruteProtection) {
// Don't brute-force protect it, just do HTTP authentication
authorizedCallback(false);
} else if (!process.send) {
// Query data from JS object database
if (
!bruteForceDb[reqip] ||
!bruteForceDb[reqip].lastAttemptDate ||
new Date() - 300000 >= bruteForceDb[reqip].lastAttemptDate
) {
if (bruteForceDb[reqip] && bruteForceDb[reqip].invalidAttempts >= 10)
bruteForceDb[reqip] = {
invalidAttempts: 5,
};
authorizedCallback(true);
} else {
res.error(429);
logFacilities.errmessage("Brute force limit reached!");
}
} else {
var listenerEmitted = false;
// Listen for brute-force protection response
const authMessageListener = (message) => {
if (listenerEmitted) return;
if (message == "\x14AUTHA" + reqip || message == "\x14AUTHD" + reqip) {
process.removeListener("message", authMessageListener);
listenerEmitted = true;
}
if (message == "\x14AUTHD" + reqip) {
res.error(429);
logFacilities.errmessage("Brute force limit reached!");
} else if (message == "\x14AUTHA" + reqip) {
authorizedCallback(true);
}
};
process.on("message", authMessageListener);
process.send("\x12AUTHQ" + reqip);
}
} else {
next();
}
};
// IPC listener for brute force protection
module.exports.mainMessageListenerWrapper = (worker) => {
return function bruteForceListener(message) {
let ip = "";
if (message.substring(0, 6) == "\x12AUTHQ") {
ip = message.substring(6);
if (
!bruteForceDb[ip] ||
!bruteForceDb[ip].lastAttemptDate ||
new Date() - 300000 >= bruteForceDb[ip].lastAttemptDate
) {
if (bruteForceDb[ip] && bruteForceDb[ip].invalidAttempts >= 10)
bruteForceDb[ip] = {
invalidAttempts: 5,
};
worker.send("\x14AUTHA" + ip);
} else {
worker.send("\x14AUTHD" + ip);
}
} else if (message.substring(0, 6) == "\x12AUTHR") {
ip = message.substring(6);
if (bruteForceDb[ip])
bruteForceDb[ip] = {
invalidAttempts: 0,
};
} else if (message.substring(0, 6) == "\x12AUTHW") {
ip = message.substring(6);
if (!bruteForceDb[ip])
bruteForceDb[ip] = {
invalidAttempts: 0,
};
bruteForceDb[ip].invalidAttempts++;
if (bruteForceDb[ip].invalidAttempts >= 10) {
bruteForceDb[ip].lastAttemptDate = new Date();
}
}
};
};
module.exports.commands = {
stop: (args, passCommand) => {
clearInterval(passwordHashCacheIntervalId);
passCommand(args);
},
};

236
src/utils/sha256.js Normal file
View file

@ -0,0 +1,236 @@
let crypto = { __disabled__: null };
try {
crypto = require("crypto");
} catch (err) {
// Crypto support is disabled.
}
// SHA256 function
function sha256(s) {
if (crypto.__disabled__ === undefined) {
let hash = crypto.createHash("SHA256");
hash.update(s);
return hash.digest("hex");
} else {
const chrsz = 8;
const hexcase = 0;
const safeAdd = (x, y) => {
const lsw = (x & 0xffff) + (y & 0xffff);
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xffff);
};
const S = (X, n) => {
return (X >>> n) | (X << (32 - n));
};
const R = (X, n) => {
return X >>> n;
};
const Ch = (x, y, z) => {
return (x & y) ^ (~x & z);
};
const Maj = (x, y, z) => {
return (x & y) ^ (x & z) ^ (y & z);
};
const Sigma0256 = (x) => {
return S(x, 2) ^ S(x, 13) ^ S(x, 22);
};
const Sigma1256 = (x) => {
return S(x, 6) ^ S(x, 11) ^ S(x, 25);
};
const Gamma0256 = (x) => {
return S(x, 7) ^ S(x, 18) ^ R(x, 3);
};
const Gamma1256 = (x) => {
return S(x, 17) ^ S(x, 19) ^ R(x, 10);
};
function coreSha256(m, l) {
const K = new Array(
0x428a2f98,
0x71374491,
0xb5c0fbcf,
0xe9b5dba5,
0x3956c25b,
0x59f111f1,
0x923f82a4,
0xab1c5ed5,
0xd807aa98,
0x12835b01,
0x243185be,
0x550c7dc3,
0x72be5d74,
0x80deb1fe,
0x9bdc06a7,
0xc19bf174,
0xe49b69c1,
0xefbe4786,
0xfc19dc6,
0x240ca1cc,
0x2de92c6f,
0x4a7484aa,
0x5cb0a9dc,
0x76f988da,
0x983e5152,
0xa831c66d,
0xb00327c8,
0xbf597fc7,
0xc6e00bf3,
0xd5a79147,
0x6ca6351,
0x14292967,
0x27b70a85,
0x2e1b2138,
0x4d2c6dfc,
0x53380d13,
0x650a7354,
0x766a0abb,
0x81c2c92e,
0x92722c85,
0xa2bfe8a1,
0xa81a664b,
0xc24b8b70,
0xc76c51a3,
0xd192e819,
0xd6990624,
0xf40e3585,
0x106aa070,
0x19a4c116,
0x1e376c08,
0x2748774c,
0x34b0bcb5,
0x391c0cb3,
0x4ed8aa4a,
0x5b9cca4f,
0x682e6ff3,
0x748f82ee,
0x78a5636f,
0x84c87814,
0x8cc70208,
0x90befffa,
0xa4506ceb,
0xbef9a3f7,
0xc67178f2,
);
let HASH = new Array(
0x6a09e667,
0xbb67ae85,
0x3c6ef372,
0xa54ff53a,
0x510e527f,
0x9b05688c,
0x1f83d9ab,
0x5be0cd19,
);
let W = new Array(64);
let a, b, c, d, e, f, g, h, i, j;
let T1, T2;
m[l >> 5] |= 0x80 << (24 - (l % 32));
m[(((l + 64) >> 9) << 4) + 15] = l;
for (let i = 0; i < m.length; i += 16) {
a = HASH[0];
b = HASH[1];
c = HASH[2];
d = HASH[3];
e = HASH[4];
f = HASH[5];
g = HASH[6];
h = HASH[7];
for (let j = 0; j < 64; j++) {
if (j < 16) W[j] = m[j + i];
else
W[j] = safeAdd(
safeAdd(
safeAdd(Gamma1256(W[j - 2]), W[j - 7]),
Gamma0256(W[j - 15]),
),
W[j - 16],
);
T1 = safeAdd(
safeAdd(safeAdd(safeAdd(h, Sigma1256(e)), Ch(e, f, g)), K[j]),
W[j],
);
T2 = safeAdd(Sigma0256(a), Maj(a, b, c));
h = g;
g = f;
f = e;
e = safeAdd(d, T1);
d = c;
c = b;
b = a;
a = safeAdd(T1, T2);
}
HASH[0] = safeAdd(a, HASH[0]);
HASH[1] = safeAdd(b, HASH[1]);
HASH[2] = safeAdd(c, HASH[2]);
HASH[3] = safeAdd(d, HASH[3]);
HASH[4] = safeAdd(e, HASH[4]);
HASH[5] = safeAdd(f, HASH[5]);
HASH[6] = safeAdd(g, HASH[6]);
HASH[7] = safeAdd(h, HASH[7]);
}
return HASH;
}
const str2binb = (str) => {
let bin = Array();
const mask = (1 << chrsz) - 1;
for (let i = 0; i < str.length * chrsz; i += chrsz) {
bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - (i % 32));
}
return bin;
};
const Utf8Encode = (string) => {
string = string.replace(/\r\n/g, "\n");
let utftext = "";
for (let n = 0; n < string.length; n++) {
let c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if (c > 127 && c < 2048) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
};
const binb2hex = (binarray) => {
const hexTab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
let str = "";
for (let i = 0; i < binarray.length * 4; i++) {
str +=
hexTab.charAt((binarray[i >> 2] >> ((3 - (i % 4)) * 8 + 4)) & 0xf) +
hexTab.charAt((binarray[i >> 2] >> ((3 - (i % 4)) * 8)) & 0xf);
}
return str;
};
s = Utf8Encode(s);
return binb2hex(coreSha256(str2binb(s), s.length * chrsz));
}
}
module.exports = sha256;

View file

@ -0,0 +1,59 @@
const sha256 = require("../../src/utils/sha256");
const crypto = require("crypto");
// Mock the crypto module to simulate the absence of crypto support
jest.mock("crypto", () => ({
createHash: jest.fn(() => ({
update: jest.fn(),
digest: jest.fn(() => "mockedHash"),
})),
}));
describe("SHA256 hash", () => {
test("should use crypto module if available", () => {
const result = sha256("test");
expect(result).toBe("mockedHash");
expect(crypto.createHash).toHaveBeenCalledWith("SHA256");
});
test("should fallback to manual SHA256 implementation if crypto is disabled", () => {
crypto.__disabled__ = null;
const result = sha256("test");
expect(result).toBe(
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
);
});
test("should handle empty string", () => {
crypto.__disabled__ = null;
const result = sha256("");
expect(result).toBe(
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
);
});
test("should handle special characters", () => {
crypto.__disabled__ = null;
const result = sha256("!@#$%^&*()");
expect(result).toBe(
"95ce789c5c9d18490972709838ca3a9719094bca3ac16332cfec0652b0236141",
);
});
test("should handle long strings", () => {
crypto.__disabled__ = null;
const longString = "a".repeat(1000);
const result = sha256(longString);
expect(result).toBe(
"41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3",
);
});
test("should handle non-ASCII characters", () => {
crypto.__disabled__ = null;
const result = sha256("éñ");
expect(result).toBe(
"c53435f74d8215688e74112f1c6527ad31fd3b72939769a75d09a14cd8a80cfe",
);
});
});