Merge pull request #3578 from SillyTavern/whitelist-hosts Whitelist Docker hosts

f1f7a14349fa14b25d488d570deb8f19bb5f0354

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
6 files changed, +48 -11Ignore whitespace
default/config.yaml+2 -0
@@ -42,6 +42,8 @@ enableForwardedWhitelist: true
42whitelist:42whitelist:
43 - ::143 - ::1
44 - 127.0.0.144 - 127.0.0.1
45# Automatically whitelist Docker host and gateway IPs
46whitelistDockerHosts: true
45# Toggle basic authentication for endpoints47# Toggle basic authentication for endpoints
46basicAuthMode: false48basicAuthMode: false
47# Basic authentication credentials49# Basic authentication credentials
docker/docker-compose.yml+3 -1
@@ -1,10 +1,12 @@
1version: "3"
2services:1services:
3 sillytavern:2 sillytavern:
4 build: ..3 build: ..
5 container_name: sillytavern4 container_name: sillytavern
6 hostname: sillytavern5 hostname: sillytavern
7 image: ghcr.io/sillytavern/sillytavern:latest6 image: ghcr.io/sillytavern/sillytavern:latest
7 environment:
8 - NODE_ENV=production
9 - FORCE_COLOR=1
8 ports:10 ports:
9 - "8000:8000"11 - "8000:8000"
10 volumes:12 volumes:
docker/docker-entrypoint.sh+3 -0
@@ -5,5 +5,8 @@ if [ ! -e "config/config.yaml" ]; then
5 cp -r "default/config.yaml" "config/config.yaml"5 cp -r "default/config.yaml" "config/config.yaml"
6fi6fi
77
8# Execute postinstall to auto-populate config.yaml with missing values
9npm run postinstall
10
8# Start the server11# Start the server
9exec node server.js --listen "$@"12exec node server.js --listen "$@"
server.js+9 -6
@@ -41,7 +41,7 @@ import {
4141
42import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';42import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
43import basicAuthMiddleware from './src/middleware/basicAuth.js';43import basicAuthMiddleware from './src/middleware/basicAuth.js';
44import whitelistMiddleware from './src/middleware/whitelist.js';44import getWhitelistMiddleware from './src/middleware/whitelist.js';
45import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js';45import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js';
46import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';46import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
47import initRequestProxy from './src/request-proxy.js';47import initRequestProxy from './src/request-proxy.js';
@@ -125,7 +125,8 @@ if (cliArgs.listen && cliArgs.basicAuthMode) {
125}125}
126126
127if (cliArgs.whitelistMode) {127if (cliArgs.whitelistMode) {
128 app.use(whitelistMiddleware());128 const whitelistMiddleware = await getWhitelistMiddleware();
129 app.use(whitelistMiddleware);
129}130}
130131
131if (cliArgs.listen) {132if (cliArgs.listen) {
@@ -254,10 +255,12 @@ async function preSetupTasks() {
254 // Print formatted header255 // Print formatted header
255 console.log();256 console.log();
256 console.log(`SillyTavern ${version.pkgVersion}`);257 console.log(`SillyTavern ${version.pkgVersion}`);
257 console.log(version.gitBranch ? `Running '${version.gitBranch}' (${version.gitRevision}) - ${version.commitDate}` : '');258 if (version.gitBranch) {
258 if (version.gitBranch && !version.isLatest && ['staging', 'release'].includes(version.gitBranch)) {259 console.log(`Running '${version.gitBranch}' (${version.gitRevision}) - ${version.commitDate}`);
259 console.log('INFO: Currently not on the latest commit.');260 if (!version.isLatest && ['staging', 'release'].includes(version.gitBranch)) {
260 console.log(' Run \'git pull\' to update. If you have any merge conflicts, run \'git reset --hard\' and \'git pull\' to reset your branch.');261 console.log('INFO: Currently not on the latest commit.');
262 console.log(' Run \'git pull\' to update. If you have any merge conflicts, run \'git reset --hard\' and \'git pull\' to reset your branch.');
263 }
261 }264 }
262 console.log();265 console.log();
263266
src/middleware/whitelist.js+31 -3
@@ -1,14 +1,18 @@
1import path from 'node:path';1import path from 'node:path';
2import fs from 'node:fs';2import fs from 'node:fs';
3import process from 'node:process';3import process from 'node:process';
4import dns from 'node:dns';
4import Handlebars from 'handlebars';5import Handlebars from 'handlebars';
5import ipMatching from 'ip-matching';6import ipMatching from 'ip-matching';
7import isDocker from 'is-docker';
68
7import { getIpFromRequest } from '../express-common.js';9import { getIpFromRequest } from '../express-common.js';
8import { color, getConfigValue, safeReadFileSync } from '../util.js';10import { color, getConfigValue, safeReadFileSync } from '../util.js';
911
10const whitelistPath = path.join(process.cwd(), './whitelist.txt');12const whitelistPath = path.join(process.cwd(), './whitelist.txt');
11const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false, 'boolean');13const enableForwardedWhitelist = !!getConfigValue('enableForwardedWhitelist', false, 'boolean');
14const whitelistDockerHosts = !!getConfigValue('whitelistDockerHosts', true, 'boolean');
15/** @type {string[]} */
12let whitelist = getConfigValue('whitelist', []);16let whitelist = getConfigValue('whitelist', []);
1317
14if (fs.existsSync(whitelistPath)) {18if (fs.existsSync(whitelistPath)) {
@@ -46,10 +50,32 @@ function getForwardedIp(req) {
46}50}
4751
48/**52/**
53 * Resolves the IP addresses of Docker hostnames and adds them to the whitelist.
54 * @returns {Promise<void>} Promise that resolves when the Docker hostnames are resolved
55 */
56async function addDockerHostsToWhitelist() {
57 if (!whitelistDockerHosts || !isDocker()) {
58 return;
59 }
60
61 const whitelistHosts = ['host.docker.internal', 'gateway.docker.internal'];
62
63 for (const entry of whitelistHosts) {
64 try {
65 const result = await dns.promises.lookup(entry);
66 console.info(`Resolved whitelist hostname ${color.green(entry)} to IPv${result.family} address ${color.green(result.address)}`);
67 whitelist.push(result.address);
68 } catch (e) {
69 console.warn(`Failed to resolve whitelist hostname ${color.red(entry)}: ${e.message}`);
70 }
71 }
72}
73
74/**
49 * Returns a middleware function that checks if the client IP is in the whitelist.75 * Returns a middleware function that checks if the client IP is in the whitelist.
50 * @returns {import('express').RequestHandler} The middleware function76 * @returns {Promise<import('express').RequestHandler>} Promise that resolves to the middleware function
51 */77 */
52export default function whitelistMiddleware() {78export default async function getWhitelistMiddleware() {
53 const forbiddenWebpage = Handlebars.compile(79 const forbiddenWebpage = Handlebars.compile(
54 safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',80 safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',
55 );81 );
@@ -58,6 +84,8 @@ export default function whitelistMiddleware() {
58 '/favicon.ico',84 '/favicon.ico',
59 ];85 ];
6086
87 await addDockerHostsToWhitelist();
88
61 return function (req, res, next) {89 return function (req, res, next) {
62 const clientIp = getIpFromRequest(req);90 const clientIp = getIpFromRequest(req);
63 const forwardedIp = getForwardedIp(req);91 const forwardedIp = getForwardedIp(req);
src/users.js+0 -1
@@ -504,7 +504,6 @@ export function toAvatarKey(handle) {
504 */504 */
505export async function initUserStorage(dataRoot) {505export async function initUserStorage(dataRoot) {
506 console.log('Using data root:', color.green(dataRoot));506 console.log('Using data root:', color.green(dataRoot));
507 console.log();
508 await storage.init({507 await storage.init({
509 dir: path.join(dataRoot, '_storage'),508 dir: path.join(dataRoot, '_storage'),
510 ttl: false, // Never expire509 ttl: false, // Never expire