Merge pull request #3578 from SillyTavern/whitelist-hosts Whitelist Docker hosts
Signed| @@ -42,6 +42,8 @@ enableForwardedWhitelist: true | |||
| 42 | whitelist: | 42 | whitelist: |
| 43 | - ::1 | 43 | - ::1 |
| 44 | - 127.0.0.1 | 44 | - 127.0.0.1 |
| 45 | # Automatically whitelist Docker host and gateway IPs | ||
| 46 | whitelistDockerHosts: true | ||
| 45 | # Toggle basic authentication for endpoints | 47 | # Toggle basic authentication for endpoints |
| 46 | basicAuthMode: false | 48 | basicAuthMode: false |
| 47 | # Basic authentication credentials | 49 | # Basic authentication credentials |
| @@ -1,10 +1,12 @@ | |||
| 1 | version: "3" | ||
| 2 | services: | 1 | services: |
| 3 | sillytavern: | 2 | sillytavern: |
| 4 | build: .. | 3 | build: .. |
| 5 | container_name: sillytavern | 4 | container_name: sillytavern |
| 6 | hostname: sillytavern | 5 | hostname: sillytavern |
| 7 | image: ghcr.io/sillytavern/sillytavern:latest | 6 | 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: |
| @@ -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" |
| 6 | fi | 6 | fi |
| 7 | 7 | ||
| 8 | # Execute postinstall to auto-populate config.yaml with missing values | ||
| 9 | npm run postinstall | ||
| 10 | |||
| 8 | # Start the server | 11 | # Start the server |
| 9 | exec node server.js --listen "$@" | 12 | exec node server.js --listen "$@" |
| @@ -41,7 +41,7 @@ import { | |||
| 41 | 41 | ||
| 42 | import getWebpackServeMiddleware from './src/middleware/webpack-serve.js'; | 42 | import getWebpackServeMiddleware from './src/middleware/webpack-serve.js'; |
| 43 | import basicAuthMiddleware from './src/middleware/basicAuth.js'; | 43 | import basicAuthMiddleware from './src/middleware/basicAuth.js'; |
| 44 | import whitelistMiddleware from './src/middleware/whitelist.js'; | 44 | import getWhitelistMiddleware from './src/middleware/whitelist.js'; |
| 45 | import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js'; | 45 | import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js'; |
| 46 | import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; | 46 | import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; |
| 47 | import initRequestProxy from './src/request-proxy.js'; | 47 | import initRequestProxy from './src/request-proxy.js'; |
| @@ -125,7 +125,8 @@ if (cliArgs.listen && cliArgs.basicAuthMode) { | |||
| 125 | } | 125 | } |
| 126 | 126 | ||
| 127 | if (cliArgs.whitelistMode) { | 127 | if (cliArgs.whitelistMode) { |
| 128 | app.use(whitelistMiddleware()); | 128 | const whitelistMiddleware = await getWhitelistMiddleware(); |
| 129 | app.use(whitelistMiddleware); | ||
| 129 | } | 130 | } |
| 130 | 131 | ||
| 131 | if (cliArgs.listen) { | 132 | if (cliArgs.listen) { |
| @@ -254,11 +255,13 @@ async function preSetupTasks() { | |||
| 254 | // Print formatted header | 255 | // 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}`); |
| 260 | if (!version.isLatest && ['staging', 'release'].includes(version.gitBranch)) { | ||
| 259 | console.log('INFO: Currently not on the latest commit.'); | 261 | console.log('INFO: Currently not on the latest commit.'); |
| 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.'); | 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.'); |
| 261 | } | 263 | } |
| 264 | } | ||
| 262 | console.log(); | 265 | console.log(); |
| 263 | 266 | ||
| 264 | const directories = await getUserDirectoriesList(); | 267 | const directories = await getUserDirectoriesList(); |
| @@ -1,14 +1,18 @@ | |||
| 1 | import path from 'node:path'; | 1 | import path from 'node:path'; |
| 2 | import fs from 'node:fs'; | 2 | import fs from 'node:fs'; |
| 3 | import process from 'node:process'; | 3 | import process from 'node:process'; |
| 4 | import dns from 'node:dns'; | ||
| 4 | import Handlebars from 'handlebars'; | 5 | import Handlebars from 'handlebars'; |
| 5 | import ipMatching from 'ip-matching'; | 6 | import ipMatching from 'ip-matching'; |
| 7 | import isDocker from 'is-docker'; | ||
| 6 | 8 | ||
| 7 | import { getIpFromRequest } from '../express-common.js'; | 9 | import { getIpFromRequest } from '../express-common.js'; |
| 8 | import { color, getConfigValue, safeReadFileSync } from '../util.js'; | 10 | import { color, getConfigValue, safeReadFileSync } from '../util.js'; |
| 9 | 11 | ||
| 10 | const whitelistPath = path.join(process.cwd(), './whitelist.txt'); | 12 | const whitelistPath = path.join(process.cwd(), './whitelist.txt'); |
| 11 | const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false, 'boolean'); | 13 | const enableForwardedWhitelist = !!getConfigValue('enableForwardedWhitelist', false, 'boolean'); |
| 14 | const whitelistDockerHosts = !!getConfigValue('whitelistDockerHosts', true, 'boolean'); | ||
| 15 | /** @type {string[]} */ | ||
| 12 | let whitelist = getConfigValue('whitelist', []); | 16 | let whitelist = getConfigValue('whitelist', []); |
| 13 | 17 | ||
| 14 | if (fs.existsSync(whitelistPath)) { | 18 | if (fs.existsSync(whitelistPath)) { |
| @@ -46,10 +50,32 @@ function getForwardedIp(req) { | |||
| 46 | } | 50 | } |
| 47 | 51 | ||
| 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 | */ | ||
| 56 | async 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 function | 76 | * @returns {Promise<import('express').RequestHandler>} Promise that resolves to the middleware function |
| 51 | */ | 77 | */ |
| 52 | export default function whitelistMiddleware() { | 78 | export 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 | ]; |
| 60 | 86 | ||
| 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); |
| @@ -504,7 +504,6 @@ export function toAvatarKey(handle) { | |||
| 504 | */ | 504 | */ |
| 505 | export async function initUserStorage(dataRoot) { | 505 | export 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 expire | 509 | ttl: false, // Never expire |