support for Ipv6 (#2593) * 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>

da5581e20e9299eb18afb51cda92df1f8e42a7de

BPplays <58504799+BPplays@users.noreply.github.com>

Signed
4 files changed, +268 -51Ignore whitespace
default/config.yaml+14 -0
@@ -4,8 +4,18 @@ dataRoot: ./data
44# -- SERVER CONFIGURATION --
55# Listen for incoming connections
66listen: false
7+# Enables IPv6 and/or IPv4
8+protocol:
9+ ipv4: true
10+ ipv6: false
11+# Prefers IPv6 for dns, you should probably enable this on ISPs that don't have issues with IPv6
12+dnsPreferIPv6: false
13+# the hostname that autorun opens probably best left on auto. use options like 'localhost', 'st.example.com'
14+autorunHostname: "auto"
715# Server port
816port: 8000
17+# 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
18+autorunPortOverride: -1
919# -- SECURITY CONFIGURATION --
1020# Toggle whitelist mode
1121whitelistMode: true
@@ -13,6 +23,7 @@ whitelistMode: true
1323enableForwardedWhitelist: true
1424# Whitelist of allowed IP addresses
1525whitelist:
26+ - ::1
1627 - 127.0.0.1
1728# Toggle basic authentication for endpoints
1829basicAuthMode: false
@@ -35,6 +46,9 @@ securityOverride: false
3546# -- ADVANCED CONFIGURATION --
3647# Open the browser automatically
3748autorun: true
49+# Avoids using 'localhost' for autorun in auto mode.
50+# use if you don't have 'localhost' in your hosts file
51+avoidLocalhost: false
3852# Disable thumbnail generation
3953disableThumbnails: false
4054# Thumbnail quality (0-100)
package-lock.json+22 -20
@@ -58,7 +58,7 @@
5858 },
5959 "devDependencies": {
6060 "@types/jquery": "^3.5.29",
6161 "eslint": "^8.5557.0",
6262 "jquery": "^3.6.4"
6363 },
6464 "engines": {
@@ -166,9 +166,9 @@
166166 "license": "MIT"
167167 },
168168 "node_modules/@eslint/js": {
169169 "version": "8.5557.0",
170170 "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.5557.0.tgz",
171171 "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLoYs+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/nvNMpJOaJATj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
172172 "dev": true,
173173 "license": "MIT",
174174 "engines": {
@@ -185,14 +185,15 @@
185185 }
186186 },
187187 "node_modules/@humanwhocodes/config-array": {
188188 "version": "0.11.1314",
189189 "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.1314.tgz",
190190 "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
191+ "deprecated": "Use @eslint/config-array instead",
191192 "dev": true,
192193 "license": "Apache-2.0",
193194 "dependencies": {
194195 "@humanwhocodes/object-schema": "^2.0.12",
195196 "debug": "^4.13.1",
196197 "minimatch": "^3.0.5"
197198 },
198199 "engines": {
@@ -200,9 +201,9 @@
200201 }
201202 },
202203 "node_modules/@humanwhocodes/config-array/node_modules/debug": {
203204 "version": "4.3.46",
204205 "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.46.tgz",
205206 "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7GO/vCNNhehwxfkQ09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
206207 "dev": true,
207208 "license": "MIT",
208209 "dependencies": {
@@ -239,9 +240,10 @@
239240 }
240241 },
241242 "node_modules/@humanwhocodes/object-schema": {
242243 "version": "2.0.13",
243244 "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.13.tgz",
244245 "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+1GWiExi9QyzlD8x/yTvvLDHpoxLr0xxxeslWw08Zt7wIKcDcA==",
246+ "deprecated": "Use @eslint/object-schema instead",
245247 "dev": true,
246248 "license": "BSD-3-Clause"
247249 },
@@ -2455,17 +2457,17 @@
24552457 }
24562458 },
24572459 "node_modules/eslint": {
24582460 "version": "8.5557.0",
24592461 "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.5557.0.tgz",
24602462 "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdtdZ6+rCntju0xEH7teIABPwXpahftIaTdAmexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
24612463 "dev": true,
24622464 "license": "MIT",
24632465 "dependencies": {
24642466 "@eslint-community/eslint-utils": "^4.2.0",
24652467 "@eslint-community/regexpp": "^4.6.1",
24662468 "@eslint/eslintrc": "^2.1.4",
24672469 "@eslint/js": "8.5557.0",
24682470 "@humanwhocodes/config-array": "^0.11.1314",
24692471 "@humanwhocodes/module-importer": "^1.0.1",
24702472 "@nodelib/fs.walk": "^1.2.8",
24712473 "@ungap/structured-clone": "^1.2.0",
package.json+1 -1
@@ -90,7 +90,7 @@
9090 "main": "server.js",
9191 "devDependencies": {
9292 "@types/jquery": "^3.5.29",
9393 "eslint": "^8.5557.0",
9494 "jquery": "^3.6.4"
9595 }
9696}
server.js+231 -30
@@ -54,8 +54,9 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
5454 if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
5555}
5656
57-// Set default DNS resolution order to IPv4 first
57+
58-dns.setDefaultResultOrder('ipv4first');
58+
59+
5960
6061const DEFAULT_PORT = 8000;
6162const DEFAULT_AUTORUN = false;
@@ -66,16 +67,46 @@ const DEFAULT_ACCOUNTS = false;
6667const DEFAULT_CSRF_DISABLED = false;
6768const DEFAULT_BASIC_AUTH = false;
6869
70+const DEFAULT_ENABLE_IPV6 = false;
71+const DEFAULT_ENABLE_IPV4 = true;
72+
73+const DEFAULT_PREFER_IPV6 = false;
74+
75+const DEFAULT_AVOID_LOCALHOST = false;
76+
77+const DEFAULT_AUTORUN_HOSTNAME = 'auto';
78+const DEFAULT_AUTORUN_PORT = -1;
79+
6980const cliArguments = yargs(hideBin(process.argv))
7081 .usage('Usage: <your-start-script> <command> [options]')
7182 .option('portenableIPv6', {
83+ type: 'boolean',
84+ default: null,
85+ describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,
86+ }).option('enableIPv4', {
87+ type: 'boolean',
88+ default: null,
89+ describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,
90+ }).option('port', {
7291 type: 'number',
7392 default: null,
7493 describe: `Sets the port under which SillyTavern will run.\nIf not provided falls back to yaml config 'port'.\n[config default: ${DEFAULT_PORT}]`,
94+ }).option('dnsPreferIPv6', {
95+ type: 'boolean',
96+ default: null,
97+ 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}]`,
7598 }).option('autorun', {
7699 type: 'boolean',
77100 default: null,
78101 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}]`,
102+ }).option('autorunHostname', {
103+ type: 'string',
104+ default: null,
105+ describe: 'the autorun hostname, probably best left on \'auto\'.\nuse values like \'localhost\', \'st.example.com\'',
106+ }).option('autorunPortOverride', {
107+ type: 'string',
108+ default: null,
109+ 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',
79110 }).option('listen', {
80111 type: 'boolean',
81112 default: null,
@@ -108,6 +139,10 @@ const cliArguments = yargs(hideBin(process.argv))
108139 type: 'string',
109140 default: null,
110141 describe: 'Root directory for data storage',
142+ }).option('avoidLocalhost', {
143+ type: 'boolean',
144+ default: null,
145+ describe: 'Avoids using \'localhost\' for autorun in auto mode.\nuse if you don\'t have \'localhost\' in your hosts file',
111146 }).option('basicAuthMode', {
112147 type: 'boolean',
113148 default: null,
@@ -138,6 +173,31 @@ const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
138173
139174const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);
140175
176+const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
177+const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
178+
179+const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
180+const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
181+
182+const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);
183+
184+const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);
185+
186+if (dnsPreferIPv6) {
187+ // Set default DNS resolution order to IPv6 first
188+ dns.setDefaultResultOrder('ipv6first');
189+ console.log('Preferring IPv6 for DNS resolution');
190+} else {
191+ // Set default DNS resolution order to IPv4 first
192+ dns.setDefaultResultOrder('ipv4first');
193+ console.log('Preferring IPv4 for DNS resolution');
194+}
195+
196+if (!enableIPv6 && !enableIPv4) {
197+ console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
198+ process.exit(1);
199+}
200+
141201// CORS Settings //
142202const CORS = cors({
143203 origin: 'null',
@@ -546,18 +606,19 @@ app.use('/api/speech', require('./src/endpoints/speech').router);
546606// Azure TTS
547607app.use('/api/azure', require('./src/endpoints/azure').router);
548608
549609const tavernUrltavernUrlV6 = new URL(
550610 (cliArguments.ssl ? 'https://' : 'http://') +
551611 (listen ? '0.0.0.0[::]' : '127.0.0.[::1]') +
552612 (':' + server_port),
553613);
554614
555615const autorunUrltavernUrl = new URL(
556616 (cliArguments.ssl ? 'https://' : 'http://') +
557617 (listen ? '0.0.0.0' : '127.0.0.1') +
558618 (':' + server_port),
559619);
560620
621+
561622/**
562623 * Tasks that need to be run before the server starts listening.
563624 */
@@ -606,20 +667,83 @@ const preSetupTasks = async function () {
606667 });
607668};
608669
670+function removeColorFormatting(text) {
671+ // ANSI escape codes for colors are usually in the format \x1b[<codes>m
672+ return text.replace(/\x1b\[\d{1,2}(;\d{1,2})*m/g, '');
673+}
674+
675+function getSeparator(n) {
676+ return '='.repeat(n);
677+}
678+
679+
680+
681+function getAutorunHostname() {
682+
683+ if (autorunHostname === 'auto') {
684+ if (enableIPv6 && enableIPv4) {
685+ if (avoidLocalhost) return '[::1]';
686+ return 'localhost';
687+ }
688+
689+ if (enableIPv6) {
690+ return '[::1]';
691+ }
692+
693+ if (enableIPv4) {
694+ return '127.0.0.1';
695+ }
696+ }
697+
698+ return autorunHostname;
699+}
700+
701+
609702/**
610703 * Tasks that need to be run after the server starts listening.
611704 */
612705const postSetupTasks = async function (v6Failed, v4Failed) {
706+
707+
708+ const autorunUrl = new URL(
709+ (cliArguments.ssl ? 'https://' : 'http://') +
710+ (getAutorunHostname()) +
711+ (':') +
712+ ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),
713+ );
714+
715+
613716 console.log('Launching...');
614717
615718 if (autorun) open(autorunUrl.toString());
616719
617720 setWindowTitle('SillyTavern WebServer');
618721
619- console.log(color.green('SillyTavern is listening on: ' + tavernUrl));
722+
723+ let ipv6Color = color.green;
724+ let ipv4Color = color.green;
725+ let autorunColor = color.blue;
726+
727+ let logListen = 'SillyTavern is listening on';
728+
729+ if (enableIPv6 && !v6Failed) {
730+ logListen += ipv6Color(' IPv6: ' + tavernUrlV6.host);
731+ }
732+
733+ if (enableIPv4 && !v4Failed) {
734+ logListen += ipv4Color(' IPv4: ' + tavernUrl.host);
735+ }
736+
737+ let goToLog = 'Go to: ' + autorunColor(autorunUrl) + ' to open SillyTavern';
738+ let plainGoToLog = removeColorFormatting(goToLog);
739+
740+ console.log(logListen);
741+ console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
742+ console.log(goToLog);
743+ console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
620744
621745 if (listen) {
622746 console.log('\n0[::] 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');
623747 }
624748
625749 if (basicAuthMode) {
@@ -674,6 +798,102 @@ function logSecurityAlert(message) {
674798 process.exit(1);
675799}
676800
801+
802+
803+function handleServerListenFail(v6Failed, v4Failed) {
804+ if (v6Failed && !enableIPv4) {
805+ console.error('fatal error: Failed to start server on IPv6 and IPv4 disabled');
806+ process.exit(1);
807+ }
808+
809+ if (v4Failed && !enableIPv6) {
810+ console.error('fatal error: Failed to start server on IPv4 and IPv6 disabled');
811+ process.exit(1);
812+ }
813+
814+ if (v6Failed && v4Failed) {
815+ console.error('fatal error: Failed to start server on both IPv6 and IPv4');
816+ process.exit(1);
817+ }
818+}
819+
820+
821+function createHttpsServer(url) {
822+ return new Promise((resolve, reject) => {
823+ const server = https.createServer(
824+ {
825+ cert: fs.readFileSync(cliArguments.certPath),
826+ key: fs.readFileSync(cliArguments.keyPath),
827+ }, app);
828+ server.on('error', reject);
829+ server.on('listening', resolve);
830+ server.listen(url.port, url.hostname);
831+ });
832+}
833+
834+function createHttpServer(url) {
835+ return new Promise((resolve, reject) => {
836+ const server = http.createServer(app);
837+ server.on('error', reject);
838+ server.on('listening', resolve);
839+ server.listen(url.port, url.hostname);
840+ });
841+}
842+
843+
844+
845+
846+async function startHTTPorHTTPS() {
847+ let v6Failed = false;
848+ let v4Failed = false;
849+
850+ let createFunc = createHttpServer;
851+ if (cliArguments.ssl) {
852+ createFunc = createHttpsServer;
853+ }
854+
855+ if (enableIPv6) {
856+ try {
857+ await createFunc(tavernUrlV6);
858+ } catch(error) {
859+ if (enableIPv4) {
860+ console.error('non-fatal error: failed to start server on IPv6', error);
861+ }
862+
863+ v6Failed = true;
864+ }
865+ }
866+
867+ if (enableIPv4) {
868+ try {
869+ await createFunc(tavernUrl);
870+ } catch(error) {
871+ if (enableIPv6) {
872+ console.error('non-fatal error: failed to start server on IPv4', error);
873+ }
874+
875+ v4Failed = true;
876+ }
877+ }
878+ return [v6Failed, v4Failed];
879+}
880+
881+
882+
883+
884+async function startServer() {
885+ let v6Failed = false;
886+ let v4Failed = false;
887+
888+
889+ [v6Failed, v4Failed] = await startHTTPorHTTPS();
890+
891+ handleServerListenFail(v6Failed, v4Failed);
892+ postSetupTasks(v6Failed, v4Failed);
893+}
894+
895+
896+
677897async function verifySecuritySettings() {
678898 // Skip all security checks as listen is set to false
679899 if (!listen) {
@@ -707,23 +927,4 @@ userModule.initUserStorage(dataRoot)
707927 .then(userModule.migrateUserData)
708928 .then(verifySecuritySettings)
709929 .then(preSetupTasks)
710930 .finally((startServer) => {;
711- if (cliArguments.ssl) {
712- https.createServer(
713- {
714- cert: fs.readFileSync(cliArguments.certPath),
715- key: fs.readFileSync(cliArguments.keyPath),
716- }, app)
717- .listen(
718- Number(tavernUrl.port) || 443,
719- tavernUrl.hostname,
720- postSetupTasks,
721- );
722- } else {
723- http.createServer(app).listen(
724- Number(tavernUrl.port) || 80,
725- tavernUrl.hostname,
726- postSetupTasks,
727- );
728- }
729- });