From da5581e20e9299eb18afb51cda92df1f8e42a7de Mon Sep 17 00:00:00 2001 From: BPplays <58504799+BPplays@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:12:12 -0700 Subject: [PATCH] support for Ipv6 (#2593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * started adding v6 support * added error checking and change messages to the user * fixed lsp caused issue * fixed formatting error * added error handling to https * fixed formatting errors * brought server starting into different func and added enable v6 and v4 * added error checking for disabling both v6 and v4. added option to prefer v6 for dns. added that stuff to the default config * fixed dumb bug * changed to settings named disable ipvx * fixed failed ips still showing as listening * fixed error handling * changed ip protocol config layout * small const name changes * fixed no error if only available protocol fails, and changed wording of some errors * fixed error handling saying 'non-fatal error' for protocol fail even when it's the only one enabled * moved more logic to listen error handler * fixed eslint issues * added more info on when to prefer ipv6 for dns * in conf changed one 'ipv6' to 'IPv6' for consistency * changed error message and redid how starting the server works * removed unneeded log * removed unneeded log * removed unneeded comments * fixed errors * fixed errors * fixed errors * changed the wording of ip related error messages * removed empty lines * changed to .finally(startServer); * removed some whitespace * disabled ipv6 by default ╯︿╰ and changed some message wording * added auto mode for autorun hostname and changed formatting for listening log and added goto message with autorun url * added autorun port override * removed debug log * changed formatting * added cli args to ipv6 and autorun stuff * moved cli args around * changed formatting * changed colors for ip * added avoidLocalhost cli arg * changed formatting * changed to not print protocol on listening * added config option for avoid localhost and changed formatting of messages * fixed avoid localhost config option * Fix ipv4 color --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com> --- default/config.yaml | 14 +++ package-lock.json | 42 +++---- package.json | 2 +- server.js | 263 ++++++++++++++++++++++++++++++++++++++------ 4 files changed, 269 insertions(+), 52 deletions(-) diff --git a/default/config.yaml b/default/config.yaml index 354c295ff..55ef604b6 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -4,8 +4,18 @@ dataRoot: ./data # -- SERVER CONFIGURATION -- # Listen for incoming connections listen: false +# Enables IPv6 and/or IPv4 +protocol: + ipv4: true + ipv6: false +# Prefers IPv6 for dns, you should probably enable this on ISPs that don't have issues with IPv6 +dnsPreferIPv6: false +# the hostname that autorun opens probably best left on auto. use options like 'localhost', 'st.example.com' +autorunHostname: "auto" # Server port port: 8000 +# overrides the port for autorun with open your browser with this port and ignore what port the server is running on. -1 is use server port +autorunPortOverride: -1 # -- SECURITY CONFIGURATION -- # Toggle whitelist mode whitelistMode: true @@ -13,6 +23,7 @@ whitelistMode: true enableForwardedWhitelist: true # Whitelist of allowed IP addresses whitelist: + - ::1 - 127.0.0.1 # Toggle basic authentication for endpoints basicAuthMode: false @@ -35,6 +46,9 @@ securityOverride: false # -- ADVANCED CONFIGURATION -- # Open the browser automatically autorun: true +# Avoids using 'localhost' for autorun in auto mode. +# use if you don't have 'localhost' in your hosts file +avoidLocalhost: false # Disable thumbnail generation disableThumbnails: false # Thumbnail quality (0-100) diff --git a/package-lock.json b/package-lock.json index 3862aa21f..f27b2d3e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,7 @@ }, "devDependencies": { "@types/jquery": "^3.5.29", - "eslint": "^8.55.0", + "eslint": "^8.57.0", "jquery": "^3.6.4" }, "engines": { @@ -166,9 +166,9 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", - "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "license": "MIT", "engines": { @@ -185,14 +185,15 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -200,9 +201,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "license": "MIT", "dependencies": { @@ -239,9 +240,10 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true, "license": "BSD-3-Clause" }, @@ -2455,17 +2457,17 @@ } }, "node_modules/eslint": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", - "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.55.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", diff --git a/package.json b/package.json index 7ef4da6b6..ac113a7e6 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "main": "server.js", "devDependencies": { "@types/jquery": "^3.5.29", - "eslint": "^8.55.0", + "eslint": "^8.57.0", "jquery": "^3.6.4" } } diff --git a/server.js b/server.js index 766629b37..238c9a715 100644 --- a/server.js +++ b/server.js @@ -54,8 +54,9 @@ if (process.versions && process.versions.node && process.versions.node.match(/20 if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false); } -// Set default DNS resolution order to IPv4 first -dns.setDefaultResultOrder('ipv4first'); + + + const DEFAULT_PORT = 8000; const DEFAULT_AUTORUN = false; @@ -66,16 +67,46 @@ const DEFAULT_ACCOUNTS = false; const DEFAULT_CSRF_DISABLED = false; const DEFAULT_BASIC_AUTH = false; +const DEFAULT_ENABLE_IPV6 = false; +const DEFAULT_ENABLE_IPV4 = true; + +const DEFAULT_PREFER_IPV6 = false; + +const DEFAULT_AVOID_LOCALHOST = false; + +const DEFAULT_AUTORUN_HOSTNAME = 'auto'; +const DEFAULT_AUTORUN_PORT = -1; + const cliArguments = yargs(hideBin(process.argv)) .usage('Usage: [options]') - .option('port', { + .option('enableIPv6', { + type: 'boolean', + default: null, + describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`, + }).option('enableIPv4', { + type: 'boolean', + default: null, + describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`, + }).option('port', { type: 'number', default: null, describe: `Sets the port under which SillyTavern will run.\nIf not provided falls back to yaml config 'port'.\n[config default: ${DEFAULT_PORT}]`, + }).option('dnsPreferIPv6', { + type: 'boolean', + default: null, + describe: `Prefers IPv6 for dns\nyou should probably have the enabled if you're on an IPv6 only network\nIf not provided falls back to yaml config 'preferIPv6'.\n[config default: ${DEFAULT_PREFER_IPV6}]`, }).option('autorun', { type: 'boolean', default: null, describe: `Automatically launch SillyTavern in the browser.\nAutorun is automatically disabled if --ssl is set to true.\nIf not provided falls back to yaml config 'autorun'.\n[config default: ${DEFAULT_AUTORUN}]`, + }).option('autorunHostname', { + type: 'string', + default: null, + describe: 'the autorun hostname, probably best left on \'auto\'.\nuse values like \'localhost\', \'st.example.com\'', + }).option('autorunPortOverride', { + type: 'string', + default: null, + describe: 'Overrides the port for autorun with open your browser with this port and ignore what port the server is running on. -1 is use server port', }).option('listen', { type: 'boolean', default: null, @@ -108,6 +139,10 @@ const cliArguments = yargs(hideBin(process.argv)) type: 'string', default: null, describe: 'Root directory for data storage', + }).option('avoidLocalhost', { + type: 'boolean', + default: null, + describe: 'Avoids using \'localhost\' for autorun in auto mode.\nuse if you don\'t have \'localhost\' in your hosts file', }).option('basicAuthMode', { type: 'boolean', default: null, @@ -138,6 +173,31 @@ const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY); +const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6); +const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4); + +const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME); +const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT); + +const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6); + +const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST); + +if (dnsPreferIPv6) { + // Set default DNS resolution order to IPv6 first + dns.setDefaultResultOrder('ipv6first'); + console.log('Preferring IPv6 for DNS resolution'); +} else { + // Set default DNS resolution order to IPv4 first + dns.setDefaultResultOrder('ipv4first'); + console.log('Preferring IPv4 for DNS resolution'); +} + +if (!enableIPv6 && !enableIPv4) { + console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.'); + process.exit(1); +} + // CORS Settings // const CORS = cors({ origin: 'null', @@ -546,17 +606,18 @@ app.use('/api/speech', require('./src/endpoints/speech').router); // Azure TTS app.use('/api/azure', require('./src/endpoints/azure').router); +const tavernUrlV6 = new URL( + (cliArguments.ssl ? 'https://' : 'http://') + + (listen ? '[::]' : '[::1]') + + (':' + server_port), +); + const tavernUrl = new URL( (cliArguments.ssl ? 'https://' : 'http://') + (listen ? '0.0.0.0' : '127.0.0.1') + (':' + server_port), ); -const autorunUrl = new URL( - (cliArguments.ssl ? 'https://' : 'http://') + - ('127.0.0.1') + - (':' + server_port), -); /** * Tasks that need to be run before the server starts listening. @@ -606,20 +667,83 @@ const preSetupTasks = async function () { }); }; +function removeColorFormatting(text) { + // ANSI escape codes for colors are usually in the format \x1b[m + return text.replace(/\x1b\[\d{1,2}(;\d{1,2})*m/g, ''); +} + +function getSeparator(n) { + return '='.repeat(n); +} + + + +function getAutorunHostname() { + + if (autorunHostname === 'auto') { + if (enableIPv6 && enableIPv4) { + if (avoidLocalhost) return '[::1]'; + return 'localhost'; + } + + if (enableIPv6) { + return '[::1]'; + } + + if (enableIPv4) { + return '127.0.0.1'; + } + } + + return autorunHostname; +} + + /** * Tasks that need to be run after the server starts listening. */ -const postSetupTasks = async function () { +const postSetupTasks = async function (v6Failed, v4Failed) { + + + const autorunUrl = new URL( + (cliArguments.ssl ? 'https://' : 'http://') + + (getAutorunHostname()) + + (':') + + ((autorunPortOverride >= 0) ? autorunPortOverride : server_port), + ); + + console.log('Launching...'); if (autorun) open(autorunUrl.toString()); setWindowTitle('SillyTavern WebServer'); - console.log(color.green('SillyTavern is listening on: ' + tavernUrl)); + + let ipv6Color = color.green; + let ipv4Color = color.green; + let autorunColor = color.blue; + + let logListen = 'SillyTavern is listening on'; + + if (enableIPv6 && !v6Failed) { + logListen += ipv6Color(' IPv6: ' + tavernUrlV6.host); + } + + if (enableIPv4 && !v4Failed) { + logListen += ipv4Color(' IPv4: ' + tavernUrl.host); + } + + let goToLog = 'Go to: ' + autorunColor(autorunUrl) + ' to open SillyTavern'; + let plainGoToLog = removeColorFormatting(goToLog); + + console.log(logListen); + console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); + console.log(goToLog); + console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); if (listen) { - console.log('\n0.0.0.0 means SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If you want to limit it only to internal localhost (127.0.0.1), change the setting in config.yaml to "listen: false". Check "access.log" file in the SillyTavern directory if you want to inspect incoming connections.\n'); + console.log('[::] or 0.0.0.0 means SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If you want to limit it only to internal localhost ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false". Check "access.log" file in the SillyTavern directory if you want to inspect incoming connections.\n'); } if (basicAuthMode) { @@ -674,6 +798,102 @@ function logSecurityAlert(message) { process.exit(1); } + + +function handleServerListenFail(v6Failed, v4Failed) { + if (v6Failed && !enableIPv4) { + console.error('fatal error: Failed to start server on IPv6 and IPv4 disabled'); + process.exit(1); + } + + if (v4Failed && !enableIPv6) { + console.error('fatal error: Failed to start server on IPv4 and IPv6 disabled'); + process.exit(1); + } + + if (v6Failed && v4Failed) { + console.error('fatal error: Failed to start server on both IPv6 and IPv4'); + process.exit(1); + } +} + + +function createHttpsServer(url) { + return new Promise((resolve, reject) => { + const server = https.createServer( + { + cert: fs.readFileSync(cliArguments.certPath), + key: fs.readFileSync(cliArguments.keyPath), + }, app); + server.on('error', reject); + server.on('listening', resolve); + server.listen(url.port, url.hostname); + }); +} + +function createHttpServer(url) { + return new Promise((resolve, reject) => { + const server = http.createServer(app); + server.on('error', reject); + server.on('listening', resolve); + server.listen(url.port, url.hostname); + }); +} + + + + +async function startHTTPorHTTPS() { + let v6Failed = false; + let v4Failed = false; + + let createFunc = createHttpServer; + if (cliArguments.ssl) { + createFunc = createHttpsServer; + } + + if (enableIPv6) { + try { + await createFunc(tavernUrlV6); + } catch(error) { + if (enableIPv4) { + console.error('non-fatal error: failed to start server on IPv6', error); + } + + v6Failed = true; + } + } + + if (enableIPv4) { + try { + await createFunc(tavernUrl); + } catch(error) { + if (enableIPv6) { + console.error('non-fatal error: failed to start server on IPv4', error); + } + + v4Failed = true; + } + } + return [v6Failed, v4Failed]; +} + + + + +async function startServer() { + let v6Failed = false; + let v4Failed = false; + + + [v6Failed, v4Failed] = await startHTTPorHTTPS(); + + handleServerListenFail(v6Failed, v4Failed); + postSetupTasks(v6Failed, v4Failed); +} + + + async function verifySecuritySettings() { // Skip all security checks as listen is set to false if (!listen) { @@ -707,23 +927,4 @@ userModule.initUserStorage(dataRoot) .then(userModule.migrateUserData) .then(verifySecuritySettings) .then(preSetupTasks) - .finally(() => { - if (cliArguments.ssl) { - https.createServer( - { - cert: fs.readFileSync(cliArguments.certPath), - key: fs.readFileSync(cliArguments.keyPath), - }, app) - .listen( - Number(tavernUrl.port) || 443, - tavernUrl.hostname, - postSetupTasks, - ); - } else { - http.createServer(app).listen( - Number(tavernUrl.port) || 80, - tavernUrl.hostname, - postSetupTasks, - ); - } - }); + .finally(startServer);