Blame Raw
Cohee · 51ad27fb · · 104 lines (4.3 KB)
1 contributor
1import path from 'node:path';
2import fs from 'node:fs';
3import process from 'node:process';
4import dns from 'node:dns';
5import Handlebars from 'handlebars';
6import ipMatching from 'ip-matching';
7import isDocker from 'is-docker';
8
9import { filterValidIpPatterns, getIpFromRequest, getRealOrForwardedIp } from '../express-common.js';
10import { color, getConfigValue, safeReadFileSync } from '../util.js';
11
12const whitelistPath = path.join(process.cwd(), './whitelist.txt');
13const enableForwardedWhitelist = !!getConfigValue('enableForwardedWhitelist', false, 'boolean');
14const whitelistDockerHosts = !!getConfigValue('whitelistDockerHosts', true, 'boolean');
15/** @type {string[]} */
16let whitelist = getConfigValue('whitelist', []);
17
18if (fs.existsSync(whitelistPath)) {
19 console.warn(color.yellow('whitelist.txt is deprecated and will be removed in a future release.'));
20 console.warn(color.yellow('Please migrate its contents to the whitelist field in config.yaml. See the documentation for more details.'));
21 try {
22 let whitelistTxt = fs.readFileSync(whitelistPath, 'utf-8');
23 whitelist = whitelistTxt.split('\n').filter(ip => ip).map(ip => ip.trim());
24 } catch (e) {
25 // Ignore errors that may occur when reading the whitelist (e.g. permissions)
26 }
27}
28
29whitelist = filterValidIpPatterns(whitelist, (entry, message) => `${color.red('Warning')}: Ignoring invalid whitelist entry ${color.yellow(entry)} - ${message}`);
30
31/**
32 * Resolves the IP addresses of Docker hostnames and adds them to the whitelist.
33 * @returns {Promise<void>} Promise that resolves when the Docker hostnames are resolved
34 */
35async function addDockerHostsToWhitelist() {
36 if (!whitelistDockerHosts || !isDocker()) {
37 return;
38 }
39
40 const whitelistHosts = ['host.docker.internal', 'gateway.docker.internal'];
41
42 for (const entry of whitelistHosts) {
43 try {
44 const result = await dns.promises.lookup(entry);
45 console.info(`Resolved whitelist hostname ${color.green(entry)} to IPv${result.family} address ${color.green(result.address)}`);
46 whitelist.push(result.address);
47 } catch (e) {
48 console.warn(`Failed to resolve whitelist hostname ${color.red(entry)}: ${e.message}`);
49 }
50 }
51}
52
53/**
54 * Returns a middleware function that checks if the client IP is in the whitelist.
55 * @returns {Promise<import('express').RequestHandler>} Promise that resolves to the middleware function
56 */
57export default async function getWhitelistMiddleware() {
58 const forbiddenWebpage = Handlebars.compile(
59 safeReadFileSync(path.join(globalThis.DATA_ROOT, '_errors', 'forbidden-by-whitelist.html')) ?? '',
60 );
61
62 const noLogPaths = [
63 '/favicon.ico',
64 ];
65
66 await addDockerHostsToWhitelist();
67
68 return function (req, res, next) {
69 const clientIp = getIpFromRequest(req);
70 const forwardedIp = enableForwardedWhitelist && getRealOrForwardedIp(req);
71 const userAgent = req.headers['user-agent'];
72
73 /**
74 * Checks if an IP address matches any entry in the whitelist.
75 * @param {string[]} whitelist - The list of whitelisted IPs/CIDRs
76 * @param {string} ip - The IP address to check
77 * @returns {boolean} True if the IP matches any whitelist entry
78 */
79 function isIPInWhitelist(whitelist, ip) {
80 return whitelist.some(x => ipMatching.matches(ip, ipMatching.getMatch(x)));
81 }
82
83 //clientIp = req.connection.remoteAddress.split(':').pop();
84 if (!isIPInWhitelist(whitelist, clientIp)
85 || (forwardedIp && !isIPInWhitelist(whitelist, forwardedIp))
86 ) {
87 // Log the connection attempt with real IP address
88 const ipDetails = forwardedIp
89 ? `${clientIp} (forwarded from ${forwardedIp})`
90 : clientIp;
91
92 if (!noLogPaths.includes(req.path)) {
93 console.warn(
94 color.red(
95 `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`,
96 ),
97 );
98 }
99
100 return res.status(403).send(forbiddenWebpage({ ipDetails }));
101 }
102 next();
103 };
104}