File size: 5,317 Bytes
c120a1c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
import path from 'node:path';
import fs from 'node:fs';
import process from 'node:process';
import dns from 'node:dns';
import Handlebars from 'handlebars';
import ipMatching from 'ip-matching';
import isDocker from 'is-docker';
import { getIpFromRequest } from '../express-common.js';
import { color, getConfigValue, safeReadFileSync } from '../util.js';
const whitelistPath = path.join(process.cwd(), './whitelist.txt');
const enableForwardedWhitelist = !!getConfigValue('enableForwardedWhitelist', false, 'boolean');
const whitelistDockerHosts = !!getConfigValue('whitelistDockerHosts', true, 'boolean');
/** @type {string[]} */
let whitelist = getConfigValue('whitelist', []);
if (fs.existsSync(whitelistPath)) {
try {
let whitelistTxt = fs.readFileSync(whitelistPath, 'utf-8');
whitelist = whitelistTxt.split('\n').filter(ip => ip).map(ip => ip.trim());
} catch (e) {
// Ignore errors that may occur when reading the whitelist (e.g. permissions)
}
}
/**
* Validates and filters the whitelist, removing any invalid entries.
* @param {string[]} entries - The whitelist entries to validate
* @returns {string[]} The filtered list of valid whitelist entries
*/
function validateWhitelist(entries) {
const validEntries = [];
for (const entry of entries) {
try {
// This will throw if the entry is not a valid IP or CIDR
ipMatching.getMatch(entry);
validEntries.push(entry);
} catch (e) {
console.warn(`Whitelist ${color.red('Warning')}: Ignoring invalid entry ${color.yellow(entry)} - ${e.message}`);
}
}
return validEntries;
}
whitelist = validateWhitelist(whitelist);
/**
* Get the client IP address from the request headers.
* @param {import('express').Request} req Express request object
* @returns {string|undefined} The client IP address
*/
function getForwardedIp(req) {
if (!enableForwardedWhitelist) {
return undefined;
}
// Check if X-Real-IP is available
if (req.headers['x-real-ip']) {
return req.headers['x-real-ip'].toString();
}
// Check for X-Forwarded-For and parse if available
if (req.headers['x-forwarded-for']) {
const ipList = req.headers['x-forwarded-for'].toString().split(',').map(ip => ip.trim());
return ipList[0];
}
// If none of the headers are available, return undefined
return undefined;
}
/**
* Resolves the IP addresses of Docker hostnames and adds them to the whitelist.
* @returns {Promise<void>} Promise that resolves when the Docker hostnames are resolved
*/
async function addDockerHostsToWhitelist() {
if (!whitelistDockerHosts || !isDocker()) {
return;
}
const whitelistHosts = ['host.docker.internal', 'gateway.docker.internal'];
for (const entry of whitelistHosts) {
try {
const result = await dns.promises.lookup(entry);
console.info(`Resolved whitelist hostname ${color.green(entry)} to IPv${result.family} address ${color.green(result.address)}`);
whitelist.push(result.address);
} catch (e) {
console.warn(`Failed to resolve whitelist hostname ${color.red(entry)}: ${e.message}`);
}
}
}
/**
* Returns a middleware function that checks if the client IP is in the whitelist.
* @returns {Promise<import('express').RequestHandler>} Promise that resolves to the middleware function
*/
export default async function getWhitelistMiddleware() {
const forbiddenWebpage = Handlebars.compile(
safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',
);
const noLogPaths = [
'/favicon.ico',
];
await addDockerHostsToWhitelist();
return function (req, res, next) {
const clientIp = getIpFromRequest(req);
const forwardedIp = getForwardedIp(req);
const userAgent = req.headers['user-agent'];
/**
* Checks if an IP address matches any entry in the whitelist.
* @param {string[]} whitelist - The list of whitelisted IPs/CIDRs
* @param {string} ip - The IP address to check
* @returns {boolean} True if the IP matches any whitelist entry
*/
function isIPInWhitelist(whitelist, ip) {
return whitelist.some(x => ipMatching.matches(ip, ipMatching.getMatch(x)));
}
//clientIp = req.connection.remoteAddress.split(':').pop();
if (!isIPInWhitelist(whitelist, clientIp)
|| forwardedIp && !isIPInWhitelist(whitelist, forwardedIp)
) {
// Log the connection attempt with real IP address
const ipDetails = forwardedIp
? `${clientIp} (forwarded from ${forwardedIp})`
: clientIp;
if (!noLogPaths.includes(req.path)) {
console.warn(
color.red(
`Blocked connection from ${ipDetails}; User Agent: ${userAgent}\n\tTo allow this connection, add its IP address to the whitelist or disable whitelist mode by editing config.yaml in the root directory of your SillyTavern installation.\n`,
),
);
}
return res.status(403).send(forbiddenWebpage({ ipDetails }));
}
next();
};
}
|