Merge pull request #3283 from BPplays/ipv6_auto adds checking for if localhost resolves + JSDoc additions
Signed| @@ -70,6 +70,7 @@ import { | ||
| 70 | 70 | getSeparator, |
| 71 | 71 | stringToBool, |
| 72 | 72 | urlHostnameToIPv6, |
| 73 | + canResolve, | |
| 73 | 74 | } from './src/util.js'; |
| 74 | 75 | import { UPLOADS_DIRECTORY } from './src/constants.js'; |
| 75 | 76 | import { ensureThumbnailCache } from './src/endpoints/thumbnails.js'; |
| @@ -247,28 +248,41 @@ app.use(compression()); | ||
| 247 | 248 | app.use(responseTime()); |
| 248 | 249 | |
| 249 | 250 | |
| 251 | +/** @type {number} */ | |
| 250 | 252 | const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT); |
| 253 | +/** @type {boolean} */ | |
| 251 | 254 | const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; |
| 255 | +/** @type {boolean} */ | |
| 252 | 256 | const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); |
| 257 | +/** @type {boolean} */ | |
| 253 | 258 | const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); |
| 254 | 259 | const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); |
| 260 | +/** @type {string} */ | |
| 255 | 261 | const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); |
| 262 | +/** @type {boolean} */ | |
| 256 | 263 | const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); |
| 257 | 264 | const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); |
| 258 | 265 | const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH); |
| 266 | +/** @type {boolean} */ | |
| 259 | 267 | const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); |
| 260 | 268 | |
| 261 | 269 | const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY); |
| 262 | 270 | |
| 263 | 271 | |
| 272 | +/** @type {boolean | "auto"} */ | |
| 264 | 273 | let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6); |
| 274 | +/** @type {boolean | "auto"} */ | |
| 265 | 275 | let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4); |
| 266 | 276 | |
| 277 | +/** @type {string} */ | |
| 267 | 278 | const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME); |
| 279 | +/** @type {number} */ | |
| 268 | 280 | const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT); |
| 269 | 281 | |
| 282 | +/** @type {boolean} */ | |
| 270 | 283 | const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6); |
| 271 | 284 | |
| 285 | +/** @type {boolean} */ | |
| 272 | 286 | const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST); |
| 273 | 287 | |
| 274 | 288 | const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED); |
| @@ -382,6 +396,16 @@ function getSessionCookieAge() { | ||
| 382 | 396 | return undefined; |
| 383 | 397 | } |
| 384 | 398 | |
| 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 | + */ | |
| 385 | 409 | async function getHasIP() { |
| 386 | 410 | let hasIPv6 = false; |
| 387 | 411 | let hasIPv6Local = false; |
| @@ -395,6 +419,7 @@ async function getHasIP() { | ||
| 395 | 419 | if (iface === undefined) { |
| 396 | 420 | continue; |
| 397 | 421 | } |
| 422 | + | |
| 398 | 423 | for (const info of iface) { |
| 399 | 424 | if (info.family === 'IPv6') { |
| 400 | 425 | hasIPv6 = true; |
| @@ -413,7 +438,12 @@ async function getHasIP() { | ||
| 413 | 438 | } |
| 414 | 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 | } |
| 418 | 448 | |
| 419 | 449 | app.use(cookieSession({ |
| @@ -734,12 +764,16 @@ const preSetupTasks = async function () { | ||
| 734 | 764 | |
| 735 | 765 | /** |
| 736 | 766 | * Gets the hostname to use for autorun in the browser. |
| 737 | 767 | * @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 | |
| 738 | 770 | */ |
| 739 | 771 | async function getAutorunHostname(useIPv6, useIPv4) { |
| 740 | 772 | if (autorunHostname === 'auto') { |
| 773 | + let localhostResolve = await canResolve('localhost', useIPv6, useIPv4); | |
| 774 | + | |
| 741 | 775 | if (useIPv6 && useIPv4) { |
| 742 | 776 | if (avoidLocalhost || !localhostResolve) return '[::1]'; |
| 743 | 777 | return 'localhost'; |
| 744 | 778 | } |
| 745 | 779 | |
| @@ -752,6 +786,7 @@ function getAutorunHostname(useIPv6, useIPv4) { | ||
| 752 | 786 | } |
| 753 | 787 | } |
| 754 | 788 | |
| 789 | + | |
| 755 | 790 | return autorunHostname; |
| 756 | 791 | } |
| 757 | 792 | |
| @@ -759,11 +794,13 @@ function getAutorunHostname(useIPv6, useIPv4) { | ||
| 759 | 794 | * Tasks that need to be run after the server starts listening. |
| 760 | 795 | * @param {boolean} v6Failed If the server failed to start on IPv6 |
| 761 | 796 | * @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 | */ |
| 763 | 800 | const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { |
| 764 | 801 | const autorunUrl = new URL( |
| 765 | 802 | (cliArguments.ssl ? 'https://' : 'http://') + |
| 766 | 803 | (await getAutorunHostname(useIPv6, useIPv4)) + |
| 767 | 804 | (':') + |
| 768 | 805 | ((autorunPortOverride >= 0) ? autorunPortOverride : server_port), |
| 769 | 806 | ); |
| @@ -777,11 +814,15 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { | ||
| 777 | 814 | let logListen = 'SillyTavern is listening on'; |
| 778 | 815 | |
| 779 | 816 | if (useIPv6 && !v6Failed) { |
| 780 | 817 | logListen += color.green(' IPv6: ' + tavernUrlV6.host); |
| 818 | + ' IPv6: ' + tavernUrlV6.host | |
| 819 | + ); | |
| 781 | 820 | } |
| 782 | 821 | |
| 783 | 822 | if (useIPv4 && !v4Failed) { |
| 784 | 823 | logListen += color.green(' IPv4: ' + tavernUrl.host); |
| 824 | + ' IPv4: ' + tavernUrl.host | |
| 825 | + ); | |
| 785 | 826 | } |
| 786 | 827 | |
| 787 | 828 | const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern'; |
| @@ -793,16 +834,22 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { | ||
| 793 | 834 | console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); |
| 794 | 835 | |
| 795 | 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 | } |
| 798 | 841 | |
| 799 | 842 | if (basicAuthMode) { |
| 800 | 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 | 847 | } else if (!perUserBasicAuth) { |
| 803 | 848 | const basicAuthUser = getConfigValue('basicAuthUser', {}); |
| 804 | 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 | 902 | * Handles the case where the server failed to start on one or both protocols. |
| 856 | 903 | * @param {boolean} v6Failed If the server failed to start on IPv6 |
| 857 | 904 | * @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 | */ |
| 859 | 908 | function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) { |
| 860 | 909 | if (v6Failed && !useIPv4) { |
| @@ -876,6 +925,7 @@ function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) { | ||
| 876 | 925 | /** |
| 877 | 926 | * Creates an HTTPS server. |
| 878 | 927 | * @param {URL} url The URL to listen on |
| 928 | + * @param {number} ipVersion the ip version to use | |
| 879 | 929 | * @returns {Promise<void>} A promise that resolves when the server is listening |
| 880 | 930 | * @throws {Error} If the server fails to start |
| 881 | 931 | */ |
| @@ -903,6 +953,7 @@ function createHttpsServer(url, ipVersion) { | ||
| 903 | 953 | /** |
| 904 | 954 | * Creates an HTTP server. |
| 905 | 955 | * @param {URL} url The URL to listen on |
| 956 | + * @param {number} ipVersion the ip version to use | |
| 906 | 957 | * @returns {Promise<void>} A promise that resolves when the server is listening |
| 907 | 958 | * @throws {Error} If the server fails to start |
| 908 | 959 | */ |
| @@ -923,6 +974,12 @@ function createHttpServer(url, ipVersion) { | ||
| 923 | 974 | }); |
| 924 | 975 | } |
| 925 | 976 | |
| 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 | + */ | |
| 926 | 983 | async function startHTTPorHTTPS(useIPv6, useIPv4) { |
| 927 | 984 | let v6Failed = false; |
| 928 | 985 | let v4Failed = false; |
| @@ -957,12 +1014,19 @@ async function startHTTPorHTTPS(useIPv6, useIPv4) { | ||
| 957 | 1014 | async function startServer() { |
| 958 | 1015 | let useIPv6 = (enableIPv6 === true); |
| 959 | 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; | |
| 961 | 1024 | |
| 962 | 1025 | |
| 963 | 1026 | if (enableIPv6 === 'auto' || enableIPv4 === 'auto') { |
| 964 | 1027 | [hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local] = await getHasIP(); |
| 965 | 1028 | |
| 1029 | + | |
| 966 | 1030 | hasIPv6 = listen ? hasIPv6Any : hasIPv6Local; |
| 967 | 1031 | if (enableIPv6 === 'auto') { |
| 968 | 1032 | useIPv6 = hasIPv6; |
| @@ -987,10 +1051,6 @@ async function startServer() { | ||
| 987 | 1051 | console.log('IPv4 support detected (but disabled)'); |
| 988 | 1052 | } |
| 989 | 1053 | } |
| 990 | - } else { | |
| 991 | - console.log('Neither protocol: ipv6, nor ipv4 are set to auto, skipping detection'); | |
| 992 | - } | |
| 993 | - | |
| 994 | 1054 | |
| 995 | 1055 | |
| 996 | 1056 | if (enableIPv6 === 'auto' && enableIPv4 === 'auto') { |
| @@ -1000,14 +1060,23 @@ async function startServer() { | ||
| 1000 | 1060 | } |
| 1001 | 1061 | } |
| 1002 | 1062 | |
| 1063 | + } else { | |
| 1064 | + console.log('Neither protocol: ipv6, nor ipv4 are set to auto, skipping detection'); | |
| 1065 | + } | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
| 1069 | + | |
| 1003 | 1070 | if (!useIPv6 && !useIPv4) { |
| 1004 | 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 | 1072 | process.exit(1); |
| 1006 | 1073 | } |
| 1007 | 1074 | |
| 1075 | + | |
| 1008 | 1076 | const [v6Failed, v4Failed] = await startHTTPorHTTPS(useIPv6, useIPv4); |
| 1009 | 1077 | |
| 1010 | 1078 | handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4); |
| 1079 | + | |
| 1011 | 1080 | postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4); |
| 1012 | 1081 | } |
| 1013 | 1082 | |
| @@ -5,6 +5,7 @@ import process from 'node:process'; | ||
| 5 | 5 | import { Readable } from 'node:stream'; |
| 6 | 6 | import { createRequire } from 'node:module'; |
| 7 | 7 | import { Buffer } from 'node:buffer'; |
| 8 | +import { promises as dnsPromise } from 'node:dns'; | |
| 8 | 9 | |
| 9 | 10 | import yaml from 'yaml'; |
| 10 | 11 | import { sync as commandExistsSync } from 'command-exists'; |
| @@ -692,6 +693,11 @@ export function isValidUrl(url) { | ||
| 692 | 693 | } |
| 693 | 694 | } |
| 694 | 695 | |
| 696 | +/** | |
| 697 | + * removes starting `[` or ending `]` from hostname. | |
| 698 | + * @param {string} hostname hostname to use | |
| 699 | + * @returns {string} hostname plus the modifications | |
| 700 | + */ | |
| 695 | 701 | export function urlHostnameToIPv6(hostname) { |
| 696 | 702 | if (hostname.startsWith('[')) { |
| 697 | 703 | hostname = hostname.slice(1); |
| @@ -702,6 +708,49 @@ export function urlHostnameToIPv6(hostname) { | ||
| 702 | 708 | return hostname; |
| 703 | 709 | } |
| 704 | 710 | |
| 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 | + */ | |
| 705 | 754 | export function stringToBool(str) { |
| 706 | 755 | if (str === 'true') return true; |
| 707 | 756 | if (str === 'false') return false; |