Merge pull request #3283 from BPplays/ipv6_auto adds checking for if localhost resolves + JSDoc additions

d78c8b7cc881e9502c78712f6567a7bee8e305fd

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

Signed
2 files changed, +135 -17Ignore whitespace
server.js+86 -17
@@ -70,6 +70,7 @@ import {
70 getSeparator,70 getSeparator,
71 stringToBool,71 stringToBool,
72 urlHostnameToIPv6,72 urlHostnameToIPv6,
73 canResolve,
73} from './src/util.js';74} from './src/util.js';
74import { UPLOADS_DIRECTORY } from './src/constants.js';75import { UPLOADS_DIRECTORY } from './src/constants.js';
75import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';76import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
@@ -247,28 +248,41 @@ app.use(compression());
247app.use(responseTime());248app.use(responseTime());
248249
249250
251/** @type {number} */
250const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);252const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);
253/** @type {boolean} */
251const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;254const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
255/** @type {boolean} */
252const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);256const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
257/** @type {boolean} */
253const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);258const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
254const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);259const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);
260/** @type {string} */
255const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');261const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
262/** @type {boolean} */
256const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);263const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
257const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);264const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
258const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);265const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);
266/** @type {boolean} */
259const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);267const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
260268
261const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);269const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);
262270
263271
272/** @type {boolean | "auto"} */
264let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);273let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
274/** @type {boolean | "auto"} */
265let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);275let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
266276
277/** @type {string} */
267const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);278const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
279/** @type {number} */
268const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);280const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
269281
282/** @type {boolean} */
270const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);283const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);
271284
285/** @type {boolean} */
272const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);286const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);
273287
274const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED);288const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED);
@@ -382,6 +396,16 @@ function getSessionCookieAge() {
382 return undefined;396 return undefined;
383}397}
384398
399
400/**
401 * Checks the network interfaces to determine the presence of IPv6 and IPv4 addresses.
402 *
403 * @returns {Promise<[boolean, boolean, boolean, boolean]>} A promise that resolves to an array containing:
404 * - [0]: `hasIPv6` (boolean) - Whether the computer has any IPv6 address, including (`::1`).
405 * - [1]: `hasIPv4` (boolean) - Whether the computer has any IPv4 address, including (`127.0.0.1`).
406 * - [2]: `hasIPv6Local` (boolean) - Whether the computer has local IPv6 address (`::1`).
407 * - [3]: `hasIPv4Local` (boolean) - Whether the computer has local IPv4 address (`127.0.0.1`).
408 */
385async function getHasIP() {409async function getHasIP() {
386 let hasIPv6 = false;410 let hasIPv6 = false;
387 let hasIPv6Local = false;411 let hasIPv6Local = false;
@@ -395,6 +419,7 @@ async function getHasIP() {
395 if (iface === undefined) {419 if (iface === undefined) {
396 continue;420 continue;
397 }421 }
422
398 for (const info of iface) {423 for (const info of iface) {
399 if (info.family === 'IPv6') {424 if (info.family === 'IPv6') {
400 hasIPv6 = true;425 hasIPv6 = true;
@@ -413,7 +438,12 @@ async function getHasIP() {
413 }438 }
414 if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;439 if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
415 }440 }
416 return [hasIPv6, hasIPv4, hasIPv6Local, hasIPv4Local];441 return [
442 hasIPv6,
443 hasIPv4,
444 hasIPv6Local,
445 hasIPv4Local,
446 ];
417}447}
418448
419app.use(cookieSession({449app.use(cookieSession({
@@ -734,12 +764,16 @@ const preSetupTasks = async function () {
734764
735/**765/**
736 * Gets the hostname to use for autorun in the browser.766 * Gets the hostname to use for autorun in the browser.
737 * @returns {string} The hostname to use for autorun767 * @param {boolean} useIPv6 If use IPv6
768 * @param {boolean} useIPv4 If use IPv4
769 * @returns Promise<string> The hostname to use for autorun
738 */770 */
739function getAutorunHostname(useIPv6, useIPv4) {771async function getAutorunHostname(useIPv6, useIPv4) {
740 if (autorunHostname === 'auto') {772 if (autorunHostname === 'auto') {
773 let localhostResolve = await canResolve('localhost', useIPv6, useIPv4);
774
741 if (useIPv6 && useIPv4) {775 if (useIPv6 && useIPv4) {
742 if (avoidLocalhost) return '[::1]';776 if (avoidLocalhost || !localhostResolve) return '[::1]';
743 return 'localhost';777 return 'localhost';
744 }778 }
745779
@@ -752,6 +786,7 @@ function getAutorunHostname(useIPv6, useIPv4) {
752 }786 }
753 }787 }
754788
789
755 return autorunHostname;790 return autorunHostname;
756}791}
757792
@@ -759,11 +794,13 @@ function getAutorunHostname(useIPv6, useIPv4) {
759 * Tasks that need to be run after the server starts listening.794 * Tasks that need to be run after the server starts listening.
760 * @param {boolean} v6Failed If the server failed to start on IPv6795 * @param {boolean} v6Failed If the server failed to start on IPv6
761 * @param {boolean} v4Failed If the server failed to start on IPv4796 * @param {boolean} v4Failed If the server failed to start on IPv4
797 * @param {boolean} useIPv6 If the server is using IPv6
798 * @param {boolean} useIPv4 If the server is using IPv4
762 */799 */
763const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {800const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
764 const autorunUrl = new URL(801 const autorunUrl = new URL(
765 (cliArguments.ssl ? 'https://' : 'http://') +802 (cliArguments.ssl ? 'https://' : 'http://') +
766 (getAutorunHostname(useIPv6, useIPv4)) +803 (await getAutorunHostname(useIPv6, useIPv4)) +
767 (':') +804 (':') +
768 ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),805 ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),
769 );806 );
@@ -777,11 +814,15 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
777 let logListen = 'SillyTavern is listening on';814 let logListen = 'SillyTavern is listening on';
778815
779 if (useIPv6 && !v6Failed) {816 if (useIPv6 && !v6Failed) {
780 logListen += color.green(' IPv6: ' + tavernUrlV6.host);817 logListen += color.green(
818 ' IPv6: ' + tavernUrlV6.host
819 );
781 }820 }
782821
783 if (useIPv4 && !v4Failed) {822 if (useIPv4 && !v4Failed) {
784 logListen += color.green(' IPv4: ' + tavernUrl.host);823 logListen += color.green(
824 ' IPv4: ' + tavernUrl.host
825 );
785 }826 }
786827
787 const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';828 const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';
@@ -793,16 +834,22 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
793 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');834 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
794835
795 if (listen) {836 if (listen) {
796 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');837 console.log(
838 '[::] 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'
839 );
797 }840 }
798841
799 if (basicAuthMode) {842 if (basicAuthMode) {
800 if (perUserBasicAuth && !enableAccounts) {843 if (perUserBasicAuth && !enableAccounts) {
801 console.error(color.red('Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.'));844 console.error(color.red(
845 'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.'
846 ));
802 } else if (!perUserBasicAuth) {847 } else if (!perUserBasicAuth) {
803 const basicAuthUser = getConfigValue('basicAuthUser', {});848 const basicAuthUser = getConfigValue('basicAuthUser', {});
804 if (!basicAuthUser?.username || !basicAuthUser?.password) {849 if (!basicAuthUser?.username || !basicAuthUser?.password) {
805 console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));850 console.warn(color.yellow(
851 'Basic Authentication is enabled, but username or password is not set or empty!'
852 ));
806 }853 }
807 }854 }
808 }855 }
@@ -855,6 +902,8 @@ function logSecurityAlert(message) {
855 * Handles the case where the server failed to start on one or both protocols.902 * Handles the case where the server failed to start on one or both protocols.
856 * @param {boolean} v6Failed If the server failed to start on IPv6903 * @param {boolean} v6Failed If the server failed to start on IPv6
857 * @param {boolean} v4Failed If the server failed to start on IPv4904 * @param {boolean} v4Failed If the server failed to start on IPv4
905 * @param {boolean} useIPv6 If use IPv6
906 * @param {boolean} useIPv4 If use IPv4
858 */907 */
859function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) {908function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) {
860 if (v6Failed && !useIPv4) {909 if (v6Failed && !useIPv4) {
@@ -876,6 +925,7 @@ function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) {
876/**925/**
877 * Creates an HTTPS server.926 * Creates an HTTPS server.
878 * @param {URL} url The URL to listen on927 * @param {URL} url The URL to listen on
928 * @param {number} ipVersion the ip version to use
879 * @returns {Promise<void>} A promise that resolves when the server is listening929 * @returns {Promise<void>} A promise that resolves when the server is listening
880 * @throws {Error} If the server fails to start930 * @throws {Error} If the server fails to start
881 */931 */
@@ -903,6 +953,7 @@ function createHttpsServer(url, ipVersion) {
903/**953/**
904 * Creates an HTTP server.954 * Creates an HTTP server.
905 * @param {URL} url The URL to listen on955 * @param {URL} url The URL to listen on
956 * @param {number} ipVersion the ip version to use
906 * @returns {Promise<void>} A promise that resolves when the server is listening957 * @returns {Promise<void>} A promise that resolves when the server is listening
907 * @throws {Error} If the server fails to start958 * @throws {Error} If the server fails to start
908 */959 */
@@ -923,6 +974,12 @@ function createHttpServer(url, ipVersion) {
923 });974 });
924}975}
925976
977
978/**
979 * Starts the server using http or https depending on config
980 * @param {boolean} useIPv6 If use IPv6
981 * @param {boolean} useIPv4 If use IPv4
982 */
926async function startHTTPorHTTPS(useIPv6, useIPv4) {983async function startHTTPorHTTPS(useIPv6, useIPv4) {
927 let v6Failed = false;984 let v6Failed = false;
928 let v4Failed = false;985 let v4Failed = false;
@@ -957,12 +1014,19 @@ async function startHTTPorHTTPS(useIPv6, useIPv4) {
957async function startServer() {1014async function startServer() {
958 let useIPv6 = (enableIPv6 === true);1015 let useIPv6 = (enableIPv6 === true);
959 let useIPv4 = (enableIPv4 === true);1016 let useIPv4 = (enableIPv4 === true);
960 let hasIPv6, hasIPv4, hasIPv6Local, hasIPv4Local, hasIPv6Any, hasIPv4Any;1017
1018 let hasIPv6 = false,
1019 hasIPv4 = false,
1020 hasIPv6Local = false,
1021 hasIPv4Local = false,
1022 hasIPv6Any = false,
1023 hasIPv4Any = false;
9611024
9621025
963 if (enableIPv6 === 'auto' || enableIPv4 === 'auto') {1026 if (enableIPv6 === 'auto' || enableIPv4 === 'auto') {
964 [hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local] = await getHasIP();1027 [hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local] = await getHasIP();
9651028
1029
966 hasIPv6 = listen ? hasIPv6Any : hasIPv6Local;1030 hasIPv6 = listen ? hasIPv6Any : hasIPv6Local;
967 if (enableIPv6 === 'auto') {1031 if (enableIPv6 === 'auto') {
968 useIPv6 = hasIPv6;1032 useIPv6 = hasIPv6;
@@ -987,27 +1051,32 @@ async function startServer() {
987 console.log('IPv4 support detected (but disabled)');1051 console.log('IPv4 support detected (but disabled)');
988 }1052 }
989 }1053 }
1054
1055
1056 if (enableIPv6 === 'auto' && enableIPv4 === 'auto') {
1057 if (!hasIPv6 && !hasIPv4) {
1058 console.error('Both IPv6 and IPv4 are not detected');
1059 process.exit(1);
1060 }
1061 }
1062
990 } else {1063 } else {
991 console.log('Neither protocol: ipv6, nor ipv4 are set to auto, skipping detection');1064 console.log('Neither protocol: ipv6, nor ipv4 are set to auto, skipping detection');
992 }1065 }
9931066
9941067
9951068
996 if (enableIPv6 === 'auto' && enableIPv4 === 'auto') {
997 if (!hasIPv6 && !hasIPv4) {
998 console.error('Both IPv6 and IPv4 are not detected');
999 process.exit(1);
1000 }
1001 }
10021069
1003 if (!useIPv6 && !useIPv4) {1070 if (!useIPv6 && !useIPv4) {
1004 console.error('Both IPv6 and IPv4 are disabled,\nP.S. you should never see this error, at least at one point it was checked for before this, with the rest of the config options');1071 console.error('Both IPv6 and IPv4 are disabled,\nP.S. you should never see this error, at least at one point it was checked for before this, with the rest of the config options');
1005 process.exit(1);1072 process.exit(1);
1006 }1073 }
10071074
1075
1008 const [v6Failed, v4Failed] = await startHTTPorHTTPS(useIPv6, useIPv4);1076 const [v6Failed, v4Failed] = await startHTTPorHTTPS(useIPv6, useIPv4);
10091077
1010 handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4);1078 handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4);
1079
1011 postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4);1080 postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4);
1012}1081}
10131082
src/util.js+49 -0
@@ -5,6 +5,7 @@ import process from 'node:process';
5import { Readable } from 'node:stream';5import { Readable } from 'node:stream';
6import { createRequire } from 'node:module';6import { createRequire } from 'node:module';
7import { Buffer } from 'node:buffer';7import { Buffer } from 'node:buffer';
8import { promises as dnsPromise } from 'node:dns';
89
9import yaml from 'yaml';10import yaml from 'yaml';
10import { sync as commandExistsSync } from 'command-exists';11import { sync as commandExistsSync } from 'command-exists';
@@ -692,6 +693,11 @@ export function isValidUrl(url) {
692 }693 }
693}694}
694695
696/**
697 * removes starting `[` or ending `]` from hostname.
698 * @param {string} hostname hostname to use
699 * @returns {string} hostname plus the modifications
700 */
695export function urlHostnameToIPv6(hostname) {701export function urlHostnameToIPv6(hostname) {
696 if (hostname.startsWith('[')) {702 if (hostname.startsWith('[')) {
697 hostname = hostname.slice(1);703 hostname = hostname.slice(1);
@@ -702,6 +708,49 @@ export function urlHostnameToIPv6(hostname) {
702 return hostname;708 return hostname;
703}709}
704710
711/**
712 * Test if can resolve a dns name.
713 * @param {string} name Domain name to use
714 * @param {boolean} useIPv6 If use IPv6
715 * @param {boolean} useIPv4 If use IPv4
716 * @returns Promise<boolean> If the URL is valid
717 */
718export async function canResolve(name, useIPv6 = true, useIPv4 = true) {
719 try {
720 let v6Resolved = false;
721 let v4Resolved = false;
722
723 if (useIPv6) {
724 try {
725 await dnsPromise.resolve6(name);
726 v6Resolved = true;
727 } catch (error) {
728 v6Resolved = false;
729 }
730 }
731
732 if (useIPv4) {
733 try {
734 await dnsPromise.resolve(name);
735 v4Resolved = true;
736 } catch (error) {
737 v4Resolved = false;
738 }
739 }
740
741 return v6Resolved || v4Resolved;
742
743 } catch (error) {
744 return false;
745 }
746}
747
748
749/**
750 * converts string to boolean accepts 'true' or 'false' else it returns the string put in
751 * @param {string|null} str Input string or null
752 * @returns {boolean|string|null} boolean else original input string or null if input is
753 */
705export function stringToBool(str) {754export function stringToBool(str) {
706 if (str === 'true') return true;755 if (str === 'true') return true;
707 if (str === 'false') return false;756 if (str === 'false') return false;