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, +133 -15Showing whitespace changes
server.js+84 -15
@@ -70,6 +70,7 @@ import {
7070 getSeparator,
7171 stringToBool,
7272 urlHostnameToIPv6,
73+ canResolve,
7374} from './src/util.js';
7475import { UPLOADS_DIRECTORY } from './src/constants.js';
7576import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
@@ -247,28 +248,41 @@ app.use(compression());
247248app.use(responseTime());
248249
249250
251+/** @type {number} */
250252const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);
253+/** @type {boolean} */
251254const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
255+/** @type {boolean} */
252256const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
257+/** @type {boolean} */
253258const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
254259const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);
260+/** @type {string} */
255261const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
262+/** @type {boolean} */
256263const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
257264const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
258265const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);
266+/** @type {boolean} */
259267const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
260268
261269const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);
262270
263271
272+/** @type {boolean | "auto"} */
264273let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
274+/** @type {boolean | "auto"} */
265275let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
266276
277+/** @type {string} */
267278const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
279+/** @type {number} */
268280const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
269281
282+/** @type {boolean} */
270283const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);
271284
285+/** @type {boolean} */
272286const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);
273287
274288const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED);
@@ -382,6 +396,16 @@ function getSessionCookieAge() {
382396 return undefined;
383397}
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+ */
385409async function getHasIP() {
386410 let hasIPv6 = false;
387411 let hasIPv6Local = false;
@@ -395,6 +419,7 @@ async function getHasIP() {
395419 if (iface === undefined) {
396420 continue;
397421 }
422+
398423 for (const info of iface) {
399424 if (info.family === 'IPv6') {
400425 hasIPv6 = true;
@@ -413,7 +438,12 @@ async function getHasIP() {
413438 }
414439 if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
415440 }
416- return [hasIPv6, hasIPv4, hasIPv6Local, hasIPv4Local];
441+ return [
442+ hasIPv6,
443+ hasIPv4,
444+ hasIPv6Local,
445+ hasIPv4Local,
446+ ];
417447}
418448
419449app.use(cookieSession({
@@ -734,12 +764,16 @@ const preSetupTasks = async function () {
734764
735765/**
736766 * Gets the hostname to use for autorun in the browser.
737767 * @returnsparam {stringboolean} The hostnameuseIPv6 toIf use for autorunIPv6
768+ * @param {boolean} useIPv4 If use IPv4
769+ * @returns Promise<string> The hostname to use for autorun
738770 */
739771async function getAutorunHostname(useIPv6, useIPv4) {
740772 if (autorunHostname === 'auto') {
773+ let localhostResolve = await canResolve('localhost', useIPv6, useIPv4);
774+
741775 if (useIPv6 && useIPv4) {
742776 if (avoidLocalhost || !localhostResolve) return '[::1]';
743777 return 'localhost';
744778 }
745779
@@ -752,6 +786,7 @@ function getAutorunHostname(useIPv6, useIPv4) {
752786 }
753787 }
754788
789+
755790 return autorunHostname;
756791}
757792
@@ -759,11 +794,13 @@ function getAutorunHostname(useIPv6, useIPv4) {
759794 * Tasks that need to be run after the server starts listening.
760795 * @param {boolean} v6Failed If the server failed to start on IPv6
761796 * @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
762799 */
763800const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
764801 const autorunUrl = new URL(
765802 (cliArguments.ssl ? 'https://' : 'http://') +
766803 (await getAutorunHostname(useIPv6, useIPv4)) +
767804 (':') +
768805 ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),
769806 );
@@ -777,11 +814,15 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
777814 let logListen = 'SillyTavern is listening on';
778815
779816 if (useIPv6 && !v6Failed) {
780817 logListen += color.green(' IPv6: ' + tavernUrlV6.host);
818+ ' IPv6: ' + tavernUrlV6.host
819+ );
781820 }
782821
783822 if (useIPv4 && !v4Failed) {
784823 logListen += color.green(' IPv4: ' + tavernUrl.host);
824+ ' IPv4: ' + tavernUrl.host
825+ );
785826 }
786827
787828 const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';
@@ -793,16 +834,22 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
793834 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
794835
795836 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+ );
797840 }
798841
799842 if (basicAuthMode) {
800843 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+ ));
802847 } else if (!perUserBasicAuth) {
803848 const basicAuthUser = getConfigValue('basicAuthUser', {});
804849 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+ ));
806853 }
807854 }
808855 }
@@ -855,6 +902,8 @@ function logSecurityAlert(message) {
855902 * Handles the case where the server failed to start on one or both protocols.
856903 * @param {boolean} v6Failed If the server failed to start on IPv6
857904 * @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
858907 */
859908function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) {
860909 if (v6Failed && !useIPv4) {
@@ -876,6 +925,7 @@ function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) {
876925/**
877926 * Creates an HTTPS server.
878927 * @param {URL} url The URL to listen on
928+ * @param {number} ipVersion the ip version to use
879929 * @returns {Promise<void>} A promise that resolves when the server is listening
880930 * @throws {Error} If the server fails to start
881931 */
@@ -903,6 +953,7 @@ function createHttpsServer(url, ipVersion) {
903953/**
904954 * Creates an HTTP server.
905955 * @param {URL} url The URL to listen on
956+ * @param {number} ipVersion the ip version to use
906957 * @returns {Promise<void>} A promise that resolves when the server is listening
907958 * @throws {Error} If the server fails to start
908959 */
@@ -923,6 +974,12 @@ function createHttpServer(url, ipVersion) {
923974 });
924975}
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+ */
926983async function startHTTPorHTTPS(useIPv6, useIPv4) {
927984 let v6Failed = false;
928985 let v4Failed = false;
@@ -957,12 +1014,19 @@ async function startHTTPorHTTPS(useIPv6, useIPv4) {
9571014async function startServer() {
9581015 let useIPv6 = (enableIPv6 === true);
9591016 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
9631026 if (enableIPv6 === 'auto' || enableIPv4 === 'auto') {
9641027 [hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local] = await getHasIP();
9651028
1029+
9661030 hasIPv6 = listen ? hasIPv6Any : hasIPv6Local;
9671031 if (enableIPv6 === 'auto') {
9681032 useIPv6 = hasIPv6;
@@ -987,10 +1051,6 @@ async function startServer() {
9871051 console.log('IPv4 support detected (but disabled)');
9881052 }
9891053 }
990- } else {
991- console.log('Neither protocol: ipv6, nor ipv4 are set to auto, skipping detection');
992- }
993-
9941054
9951055
9961056 if (enableIPv6 === 'auto' && enableIPv4 === 'auto') {
@@ -1000,14 +1060,23 @@ async function startServer() {
10001060 }
10011061 }
10021062
1063+ } else {
1064+ console.log('Neither protocol: ipv6, nor ipv4 are set to auto, skipping detection');
1065+ }
1066+
1067+
1068+
1069+
10031070 if (!useIPv6 && !useIPv4) {
10041071 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');
10051072 process.exit(1);
10061073 }
10071074
1075+
10081076 const [v6Failed, v4Failed] = await startHTTPorHTTPS(useIPv6, useIPv4);
10091077
10101078 handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4);
1079+
10111080 postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4);
10121081}
10131082
src/util.js+49 -0
@@ -5,6 +5,7 @@ import process from 'node:process';
55import { Readable } from 'node:stream';
66import { createRequire } from 'node:module';
77import { Buffer } from 'node:buffer';
8+import { promises as dnsPromise } from 'node:dns';
89
910import yaml from 'yaml';
1011import { sync as commandExistsSync } from 'command-exists';
@@ -692,6 +693,11 @@ export function isValidUrl(url) {
692693 }
693694}
694695
696+/**
697+ * removes starting `[` or ending `]` from hostname.
698+ * @param {string} hostname hostname to use
699+ * @returns {string} hostname plus the modifications
700+ */
695701export function urlHostnameToIPv6(hostname) {
696702 if (hostname.startsWith('[')) {
697703 hostname = hostname.slice(1);
@@ -702,6 +708,49 @@ export function urlHostnameToIPv6(hostname) {
702708 return hostname;
703709}
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+ */
718+export 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+ */
705754export function stringToBool(str) {
706755 if (str === 'true') return true;
707756 if (str === 'false') return false;