|
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'); |
|
|
|
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) { |
|
|
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function getForwardedIp(req) { |
|
if (!enableForwardedWhitelist) { |
|
return undefined; |
|
} |
|
|
|
|
|
if (req.headers['x-real-ip']) { |
|
return req.headers['x-real-ip'].toString(); |
|
} |
|
|
|
|
|
if (req.headers['x-forwarded-for']) { |
|
const ipList = req.headers['x-forwarded-for'].toString().split(',').map(ip => ip.trim()); |
|
return ipList[0]; |
|
} |
|
|
|
|
|
return undefined; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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}`); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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']; |
|
|
|
|
|
if (!whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x))) |
|
|| forwardedIp && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x))) |
|
) { |
|
|
|
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(); |
|
}; |
|
} |
|
|