Merge pull request #3431 from SillyTavern/ipv6_auto Better IPv6 support
Signed| @@ -7,6 +7,8 @@ cardsCacheCapacity: 100 | |||
| 7 | # Listen for incoming connections | 7 | # Listen for incoming connections |
| 8 | listen: false | 8 | listen: false |
| 9 | # Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled! | 9 | # Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled! |
| 10 | # - Use option "auto" to automatically detect support | ||
| 11 | # - Use true or false (no qoutes) to enable or disable each protocol | ||
| 10 | protocol: | 12 | protocol: |
| 11 | ipv4: true | 13 | ipv4: true |
| 12 | ipv6: false | 14 | ipv6: false |
| @@ -4,6 +4,7 @@ | |||
| 4 | import fs from 'node:fs'; | 4 | import fs from 'node:fs'; |
| 5 | import http from 'node:http'; | 5 | import http from 'node:http'; |
| 6 | import https from 'node:https'; | 6 | import https from 'node:https'; |
| 7 | import os from 'os'; | ||
| 7 | import path from 'node:path'; | 8 | import path from 'node:path'; |
| 8 | import util from 'node:util'; | 9 | import util from 'node:util'; |
| 9 | import net from 'node:net'; | 10 | import net from 'node:net'; |
| @@ -65,6 +66,9 @@ import { | |||
| 65 | forwardFetchResponse, | 66 | forwardFetchResponse, |
| 66 | removeColorFormatting, | 67 | removeColorFormatting, |
| 67 | getSeparator, | 68 | getSeparator, |
| 69 | stringToBool, | ||
| 70 | urlHostnameToIPv6, | ||
| 71 | canResolve, | ||
| 68 | safeReadFileSync, | 72 | safeReadFileSync, |
| 69 | setupLogLevel, | 73 | setupLogLevel, |
| 70 | } from './src/util.js'; | 74 | } from './src/util.js'; |
| @@ -150,11 +154,11 @@ const DEFAULT_PROXY_BYPASS = []; | |||
| 150 | const cliArguments = yargs(hideBin(process.argv)) | 154 | const cliArguments = yargs(hideBin(process.argv)) |
| 151 | .usage('Usage: <your-start-script> <command> [options]') | 155 | .usage('Usage: <your-start-script> <command> [options]') |
| 152 | .option('enableIPv6', { | 156 | .option('enableIPv6', { |
| 153 | type: 'boolean', | 157 | type: 'string', |
| 154 | default: null, | 158 | default: null, |
| 155 | describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`, | 159 | describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`, |
| 156 | }).option('enableIPv4', { | 160 | }).option('enableIPv4', { |
| 157 | type: 'boolean', | 161 | type: 'string', |
| 158 | default: null, | 162 | default: null, |
| 159 | describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`, | 163 | describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`, |
| 160 | }).option('port', { | 164 | }).option('port', { |
| @@ -243,27 +247,42 @@ app.use(helmet({ | |||
| 243 | app.use(compression()); | 247 | app.use(compression()); |
| 244 | app.use(responseTime()); | 248 | app.use(responseTime()); |
| 245 | 249 | ||
| 250 | |||
| 251 | /** @type {number} */ | ||
| 246 | const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT); | 252 | const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT); |
| 253 | /** @type {boolean} */ | ||
| 247 | const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; | 254 | const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; |
| 255 | /** @type {boolean} */ | ||
| 248 | const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); | 256 | const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); |
| 257 | /** @type {boolean} */ | ||
| 249 | const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); | 258 | const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); |
| 250 | const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); | 259 | const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); |
| 260 | /** @type {string} */ | ||
| 251 | const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); | 261 | const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); |
| 262 | /** @type {boolean} */ | ||
| 252 | const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); | 263 | const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); |
| 253 | const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); | 264 | const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); |
| 254 | const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH); | 265 | const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH); |
| 266 | /** @type {boolean} */ | ||
| 255 | const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); | 267 | const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); |
| 256 | 268 | ||
| 257 | const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY); | 269 | const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY); |
| 258 | 270 | ||
| 259 | const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6); | ||
| 260 | const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4); | ||
| 261 | 271 | ||
| 272 | /** @type {boolean | "auto"} */ | ||
| 273 | let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6); | ||
| 274 | /** @type {boolean | "auto"} */ | ||
| 275 | let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4); | ||
| 276 | |||
| 277 | /** @type {string} */ | ||
| 262 | const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME); | 278 | const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME); |
| 279 | /** @type {number} */ | ||
| 263 | const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT); | 280 | const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT); |
| 264 | 281 | ||
| 282 | /** @type {boolean} */ | ||
| 265 | const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6); | 283 | const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6); |
| 266 | 284 | ||
| 285 | /** @type {boolean} */ | ||
| 267 | const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST); | 286 | const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST); |
| 268 | 287 | ||
| 269 | const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED); | 288 | const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED); |
| @@ -280,7 +299,19 @@ if (dnsPreferIPv6) { | |||
| 280 | console.log('Preferring IPv4 for DNS resolution'); | 299 | console.log('Preferring IPv4 for DNS resolution'); |
| 281 | } | 300 | } |
| 282 | 301 | ||
| 283 | if (!enableIPv6 && !enableIPv4) { | 302 | |
| 303 | const ipOptions = [true, 'auto', false]; | ||
| 304 | |||
| 305 | if (!ipOptions.includes(enableIPv6)) { | ||
| 306 | console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV6); | ||
| 307 | enableIPv6 = DEFAULT_ENABLE_IPV6; | ||
| 308 | } | ||
| 309 | if (!ipOptions.includes(enableIPv4)) { | ||
| 310 | console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', ipOptions, '\n setting to:', DEFAULT_ENABLE_IPV4); | ||
| 311 | enableIPv4 = DEFAULT_ENABLE_IPV4; | ||
| 312 | } | ||
| 313 | |||
| 314 | if (enableIPv6 === false && enableIPv4 === false) { | ||
| 284 | console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.'); | 315 | console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.'); |
| 285 | process.exit(1); | 316 | process.exit(1); |
| 286 | } | 317 | } |
| @@ -365,6 +396,55 @@ function getSessionCookieAge() { | |||
| 365 | return undefined; | 396 | return undefined; |
| 366 | } | 397 | } |
| 367 | 398 | ||
| 399 | /** | ||
| 400 | * Checks the network interfaces to determine the presence of IPv6 and IPv4 addresses. | ||
| 401 | * | ||
| 402 | * @returns {Promise<[boolean, boolean, boolean, boolean]>} A promise that resolves to an array containing: | ||
| 403 | * - [0]: `hasIPv6` (boolean) - Whether the computer has any IPv6 address, including (`::1`). | ||
| 404 | * - [1]: `hasIPv4` (boolean) - Whether the computer has any IPv4 address, including (`127.0.0.1`). | ||
| 405 | * - [2]: `hasIPv6Local` (boolean) - Whether the computer has local IPv6 address (`::1`). | ||
| 406 | * - [3]: `hasIPv4Local` (boolean) - Whether the computer has local IPv4 address (`127.0.0.1`). | ||
| 407 | */ | ||
| 408 | async function getHasIP() { | ||
| 409 | let hasIPv6 = false; | ||
| 410 | let hasIPv6Local = false; | ||
| 411 | |||
| 412 | let hasIPv4 = false; | ||
| 413 | let hasIPv4Local = false; | ||
| 414 | |||
| 415 | const interfaces = os.networkInterfaces(); | ||
| 416 | |||
| 417 | for (const iface of Object.values(interfaces)) { | ||
| 418 | if (iface === undefined) { | ||
| 419 | continue; | ||
| 420 | } | ||
| 421 | |||
| 422 | for (const info of iface) { | ||
| 423 | if (info.family === 'IPv6') { | ||
| 424 | hasIPv6 = true; | ||
| 425 | if (info.address === '::1') { | ||
| 426 | hasIPv6Local = true; | ||
| 427 | } | ||
| 428 | } | ||
| 429 | |||
| 430 | if (info.family === 'IPv4') { | ||
| 431 | hasIPv4 = true; | ||
| 432 | if (info.address === '127.0.0.1') { | ||
| 433 | hasIPv4Local = true; | ||
| 434 | } | ||
| 435 | } | ||
| 436 | if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break; | ||
| 437 | } | ||
| 438 | if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break; | ||
| 439 | } | ||
| 440 | return [ | ||
| 441 | hasIPv6, | ||
| 442 | hasIPv4, | ||
| 443 | hasIPv6Local, | ||
| 444 | hasIPv4Local, | ||
| 445 | ]; | ||
| 446 | } | ||
| 447 | |||
| 368 | app.use(cookieSession({ | 448 | app.use(cookieSession({ |
| 369 | name: getCookieSessionName(), | 449 | name: getCookieSessionName(), |
| 370 | sameSite: 'strict', | 450 | sameSite: 'strict', |
| @@ -694,21 +774,23 @@ const preSetupTasks = async function () { | |||
| 694 | 774 | ||
| 695 | /** | 775 | /** |
| 696 | * Gets the hostname to use for autorun in the browser. | 776 | * Gets the hostname to use for autorun in the browser. |
| 697 | * @returns {string} The hostname to use for autorun | 777 | * @param {boolean} useIPv6 If use IPv6 |
| 778 | * @param {boolean} useIPv4 If use IPv4 | ||
| 779 | * @returns Promise<string> The hostname to use for autorun | ||
| 698 | */ | 780 | */ |
| 699 | function getAutorunHostname() { | 781 | async function getAutorunHostname(useIPv6, useIPv4) { |
| 700 | if (autorunHostname === 'auto') { | 782 | if (autorunHostname === 'auto') { |
| 701 | if (enableIPv6 && enableIPv4) { | 783 | let localhostResolve = await canResolve('localhost', useIPv6, useIPv4); |
| 702 | return avoidLocalhost | 784 | |
| 703 | ? '[::1]' | 785 | if (useIPv6 && useIPv4) { |
| 704 | : 'localhost'; | 786 | return (avoidLocalhost || !localhostResolve) ? '[::1]' : 'localhost'; |
| 705 | } | 787 | } |
| 706 | 788 | ||
| 707 | if (enableIPv6) { | 789 | if (useIPv6) { |
| 708 | return '[::1]'; | 790 | return '[::1]'; |
| 709 | } | 791 | } |
| 710 | 792 | ||
| 711 | if (enableIPv4) { | 793 | if (useIPv4) { |
| 712 | return '127.0.0.1'; | 794 | return '127.0.0.1'; |
| 713 | } | 795 | } |
| 714 | } | 796 | } |
| @@ -720,11 +802,13 @@ function getAutorunHostname() { | |||
| 720 | * Tasks that need to be run after the server starts listening. | 802 | * Tasks that need to be run after the server starts listening. |
| 721 | * @param {boolean} v6Failed If the server failed to start on IPv6 | 803 | * @param {boolean} v6Failed If the server failed to start on IPv6 |
| 722 | * @param {boolean} v4Failed If the server failed to start on IPv4 | 804 | * @param {boolean} v4Failed If the server failed to start on IPv4 |
| 805 | * @param {boolean} useIPv6 If the server is using IPv6 | ||
| 806 | * @param {boolean} useIPv4 If the server is using IPv4 | ||
| 723 | */ | 807 | */ |
| 724 | const postSetupTasks = async function (v6Failed, v4Failed) { | 808 | const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { |
| 725 | const autorunUrl = new URL( | 809 | const autorunUrl = new URL( |
| 726 | (cliArguments.ssl ? 'https://' : 'http://') + | 810 | (cliArguments.ssl ? 'https://' : 'http://') + |
| 727 | (getAutorunHostname()) + | 811 | (await getAutorunHostname(useIPv6, useIPv4)) + |
| 728 | (':') + | 812 | (':') + |
| 729 | ((autorunPortOverride >= 0) ? autorunPortOverride : server_port), | 813 | ((autorunPortOverride >= 0) ? autorunPortOverride : server_port), |
| 730 | ); | 814 | ); |
| @@ -737,12 +821,16 @@ const postSetupTasks = async function (v6Failed, v4Failed) { | |||
| 737 | 821 | ||
| 738 | let logListen = 'SillyTavern is listening on'; | 822 | let logListen = 'SillyTavern is listening on'; |
| 739 | 823 | ||
| 740 | if (enableIPv6 && !v6Failed) { | 824 | if (useIPv6 && !v6Failed) { |
| 741 | logListen += color.green(' IPv6: ' + tavernUrlV6.host); | 825 | logListen += color.green( |
| 826 | ' IPv6: ' + tavernUrlV6.host, | ||
| 827 | ); | ||
| 742 | } | 828 | } |
| 743 | 829 | ||
| 744 | if (enableIPv4 && !v4Failed) { | 830 | if (useIPv4 && !v4Failed) { |
| 745 | logListen += color.green(' IPv4: ' + tavernUrl.host); | 831 | logListen += color.green( |
| 832 | ' IPv4: ' + tavernUrl.host, | ||
| 833 | ); | ||
| 746 | } | 834 | } |
| 747 | 835 | ||
| 748 | const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern'; | 836 | const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern'; |
| @@ -754,16 +842,22 @@ const postSetupTasks = async function (v6Failed, v4Failed) { | |||
| 754 | console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); | 842 | console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); |
| 755 | 843 | ||
| 756 | if (listen) { | 844 | if (listen) { |
| 757 | 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'); | 845 | console.log( |
| 846 | '[::] 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', | ||
| 847 | ); | ||
| 758 | } | 848 | } |
| 759 | 849 | ||
| 760 | if (basicAuthMode) { | 850 | if (basicAuthMode) { |
| 761 | if (perUserBasicAuth && !enableAccounts) { | 851 | if (perUserBasicAuth && !enableAccounts) { |
| 762 | console.error(color.red('Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.')); | 852 | console.error(color.red( |
| 853 | 'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.', | ||
| 854 | )); | ||
| 763 | } else if (!perUserBasicAuth) { | 855 | } else if (!perUserBasicAuth) { |
| 764 | const basicAuthUser = getConfigValue('basicAuthUser', {}); | 856 | const basicAuthUser = getConfigValue('basicAuthUser', {}); |
| 765 | if (!basicAuthUser?.username || !basicAuthUser?.password) { | 857 | if (!basicAuthUser?.username || !basicAuthUser?.password) { |
| 766 | console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!')); | 858 | console.warn(color.yellow( |
| 859 | 'Basic Authentication is enabled, but username or password is not set or empty!', | ||
| 860 | )); | ||
| 767 | } | 861 | } |
| 768 | } | 862 | } |
| 769 | } | 863 | } |
| @@ -818,14 +912,16 @@ function logSecurityAlert(message) { | |||
| 818 | * Handles the case where the server failed to start on one or both protocols. | 912 | * Handles the case where the server failed to start on one or both protocols. |
| 819 | * @param {boolean} v6Failed If the server failed to start on IPv6 | 913 | * @param {boolean} v6Failed If the server failed to start on IPv6 |
| 820 | * @param {boolean} v4Failed If the server failed to start on IPv4 | 914 | * @param {boolean} v4Failed If the server failed to start on IPv4 |
| 915 | * @param {boolean} useIPv6 If use IPv6 | ||
| 916 | * @param {boolean} useIPv4 If use IPv4 | ||
| 821 | */ | 917 | */ |
| 822 | function handleServerListenFail(v6Failed, v4Failed) { | 918 | function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) { |
| 823 | if (v6Failed && !enableIPv4) { | 919 | if (v6Failed && !useIPv4) { |
| 824 | console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled')); | 920 | console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled')); |
| 825 | process.exit(1); | 921 | process.exit(1); |
| 826 | } | 922 | } |
| 827 | 923 | ||
| 828 | if (v4Failed && !enableIPv6) { | 924 | if (v4Failed && !useIPv6) { |
| 829 | console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled')); | 925 | console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled')); |
| 830 | process.exit(1); | 926 | process.exit(1); |
| 831 | } | 927 | } |
| @@ -839,10 +935,11 @@ function handleServerListenFail(v6Failed, v4Failed) { | |||
| 839 | /** | 935 | /** |
| 840 | * Creates an HTTPS server. | 936 | * Creates an HTTPS server. |
| 841 | * @param {URL} url The URL to listen on | 937 | * @param {URL} url The URL to listen on |
| 938 | * @param {number} ipVersion the ip version to use | ||
| 842 | * @returns {Promise<void>} A promise that resolves when the server is listening | 939 | * @returns {Promise<void>} A promise that resolves when the server is listening |
| 843 | * @throws {Error} If the server fails to start | 940 | * @throws {Error} If the server fails to start |
| 844 | */ | 941 | */ |
| 845 | function createHttpsServer(url) { | 942 | function createHttpsServer(url, ipVersion) { |
| 846 | return new Promise((resolve, reject) => { | 943 | return new Promise((resolve, reject) => { |
| 847 | const server = https.createServer( | 944 | const server = https.createServer( |
| 848 | { | 945 | { |
| @@ -851,34 +948,56 @@ function createHttpsServer(url) { | |||
| 851 | }, app); | 948 | }, app); |
| 852 | server.on('error', reject); | 949 | server.on('error', reject); |
| 853 | server.on('listening', resolve); | 950 | server.on('listening', resolve); |
| 854 | server.listen(Number(url.port || 443), url.hostname); | 951 | |
| 952 | let host = url.hostname; | ||
| 953 | if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname); | ||
| 954 | server.listen({ | ||
| 955 | host: host, | ||
| 956 | port: Number(url.port || 443), | ||
| 957 | // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used | ||
| 958 | ipv6Only: true, | ||
| 959 | }); | ||
| 855 | }); | 960 | }); |
| 856 | } | 961 | } |
| 857 | 962 | ||
| 858 | /** | 963 | /** |
| 859 | * Creates an HTTP server. | 964 | * Creates an HTTP server. |
| 860 | * @param {URL} url The URL to listen on | 965 | * @param {URL} url The URL to listen on |
| 966 | * @param {number} ipVersion the ip version to use | ||
| 861 | * @returns {Promise<void>} A promise that resolves when the server is listening | 967 | * @returns {Promise<void>} A promise that resolves when the server is listening |
| 862 | * @throws {Error} If the server fails to start | 968 | * @throws {Error} If the server fails to start |
| 863 | */ | 969 | */ |
| 864 | function createHttpServer(url) { | 970 | function createHttpServer(url, ipVersion) { |
| 865 | return new Promise((resolve, reject) => { | 971 | return new Promise((resolve, reject) => { |
| 866 | const server = http.createServer(app); | 972 | const server = http.createServer(app); |
| 867 | server.on('error', reject); | 973 | server.on('error', reject); |
| 868 | server.on('listening', resolve); | 974 | server.on('listening', resolve); |
| 869 | server.listen(Number(url.port || 80), url.hostname); | 975 | |
| 976 | let host = url.hostname; | ||
| 977 | if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname); | ||
| 978 | server.listen({ | ||
| 979 | host: host, | ||
| 980 | port: Number(url.port || 80), | ||
| 981 | // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used | ||
| 982 | ipv6Only: true, | ||
| 983 | }); | ||
| 870 | }); | 984 | }); |
| 871 | } | 985 | } |
| 872 | 986 | ||
| 873 | async function startHTTPorHTTPS() { | 987 | /** |
| 988 | * Starts the server using http or https depending on config | ||
| 989 | * @param {boolean} useIPv6 If use IPv6 | ||
| 990 | * @param {boolean} useIPv4 If use IPv4 | ||
| 991 | */ | ||
| 992 | async function startHTTPorHTTPS(useIPv6, useIPv4) { | ||
| 874 | let v6Failed = false; | 993 | let v6Failed = false; |
| 875 | let v4Failed = false; | 994 | let v4Failed = false; |
| 876 | 995 | ||
| 877 | const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer; | 996 | const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer; |
| 878 | 997 | ||
| 879 | if (enableIPv6) { | 998 | if (useIPv6) { |
| 880 | try { | 999 | try { |
| 881 | await createFunc(tavernUrlV6); | 1000 | await createFunc(tavernUrlV6, 6); |
| 882 | } catch (error) { | 1001 | } catch (error) { |
| 883 | console.error('non-fatal error: failed to start server on IPv6'); | 1002 | console.error('non-fatal error: failed to start server on IPv6'); |
| 884 | console.error(error); | 1003 | console.error(error); |
| @@ -887,9 +1006,9 @@ async function startHTTPorHTTPS() { | |||
| 887 | } | 1006 | } |
| 888 | } | 1007 | } |
| 889 | 1008 | ||
| 890 | if (enableIPv4) { | 1009 | if (useIPv4) { |
| 891 | try { | 1010 | try { |
| 892 | await createFunc(tavernUrl); | 1011 | await createFunc(tavernUrl, 4); |
| 893 | } catch (error) { | 1012 | } catch (error) { |
| 894 | console.error('non-fatal error: failed to start server on IPv4'); | 1013 | console.error('non-fatal error: failed to start server on IPv4'); |
| 895 | console.error(error); | 1014 | console.error(error); |
| @@ -902,10 +1021,59 @@ async function startHTTPorHTTPS() { | |||
| 902 | } | 1021 | } |
| 903 | 1022 | ||
| 904 | async function startServer() { | 1023 | async function startServer() { |
| 905 | const [v6Failed, v4Failed] = await startHTTPorHTTPS(); | 1024 | let useIPv6 = (enableIPv6 === true); |
| 1025 | let useIPv4 = (enableIPv4 === true); | ||
| 1026 | |||
| 1027 | let hasIPv6 = false, | ||
| 1028 | hasIPv4 = false, | ||
| 1029 | hasIPv6Local = false, | ||
| 1030 | hasIPv4Local = false, | ||
| 1031 | hasIPv6Any = false, | ||
| 1032 | hasIPv4Any = false; | ||
| 1033 | |||
| 1034 | if (enableIPv6 === 'auto' || enableIPv4 === 'auto') { | ||
| 1035 | [hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local] = await getHasIP(); | ||
| 1036 | |||
| 1037 | hasIPv6 = listen ? hasIPv6Any : hasIPv6Local; | ||
| 1038 | if (enableIPv6 === 'auto') { | ||
| 1039 | useIPv6 = hasIPv6; | ||
| 1040 | } | ||
| 1041 | if (hasIPv6) { | ||
| 1042 | if (useIPv6) { | ||
| 1043 | console.log(color.green('IPv6 support detected')); | ||
| 1044 | } else { | ||
| 1045 | console.log('IPv6 support detected (but disabled)'); | ||
| 1046 | } | ||
| 1047 | } | ||
| 1048 | |||
| 1049 | hasIPv4 = listen ? hasIPv4Any : hasIPv4Local; | ||
| 1050 | if (enableIPv4 === 'auto') { | ||
| 1051 | useIPv4 = hasIPv4; | ||
| 1052 | } | ||
| 1053 | if (hasIPv4) { | ||
| 1054 | if (useIPv4) { | ||
| 1055 | console.log(color.green('IPv4 support detected')); | ||
| 1056 | } else { | ||
| 1057 | console.log('IPv4 support detected (but disabled)'); | ||
| 1058 | } | ||
| 1059 | } | ||
| 1060 | |||
| 1061 | if (enableIPv6 === 'auto' && enableIPv4 === 'auto') { | ||
| 1062 | if (!hasIPv6 && !hasIPv4) { | ||
| 1063 | console.error('Both IPv6 and IPv4 are not detected'); | ||
| 1064 | process.exit(1); | ||
| 1065 | } | ||
| 1066 | } | ||
| 1067 | } | ||
| 1068 | |||
| 1069 | if (!useIPv6 && !useIPv4) { | ||
| 1070 | 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 | process.exit(1); | ||
| 1072 | } | ||
| 906 | 1073 | ||
| 907 | handleServerListenFail(v6Failed, v4Failed); | 1074 | const [v6Failed, v4Failed] = await startHTTPorHTTPS(useIPv6, useIPv4); |
| 908 | postSetupTasks(v6Failed, v4Failed); | 1075 | handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4); |
| 1076 | postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4); | ||
| 909 | } | 1077 | } |
| 910 | 1078 | ||
| 911 | async function verifySecuritySettings() { | 1079 | async function verifySecuritySettings() { |
| @@ -5,6 +5,7 @@ import process from 'node:process'; | |||
| 5 | import { Readable } from 'node:stream'; | 5 | import { Readable } from 'node:stream'; |
| 6 | import { createRequire } from 'node:module'; | 6 | import { createRequire } from 'node:module'; |
| 7 | import { Buffer } from 'node:buffer'; | 7 | import { Buffer } from 'node:buffer'; |
| 8 | import { promises as dnsPromise } from 'node:dns'; | ||
| 8 | 9 | ||
| 9 | import yaml from 'yaml'; | 10 | import yaml from 'yaml'; |
| 10 | import { sync as commandExistsSync } from 'command-exists'; | 11 | import { sync as commandExistsSync } from 'command-exists'; |
| @@ -695,6 +696,70 @@ export function isValidUrl(url) { | |||
| 695 | } | 696 | } |
| 696 | 697 | ||
| 697 | /** | 698 | /** |
| 699 | * removes starting `[` or ending `]` from hostname. | ||
| 700 | * @param {string} hostname hostname to use | ||
| 701 | * @returns {string} hostname plus the modifications | ||
| 702 | */ | ||
| 703 | export function urlHostnameToIPv6(hostname) { | ||
| 704 | if (hostname.startsWith('[')) { | ||
| 705 | hostname = hostname.slice(1); | ||
| 706 | } | ||
| 707 | if (hostname.endsWith(']')) { | ||
| 708 | hostname = hostname.slice(0, -1); | ||
| 709 | } | ||
| 710 | return hostname; | ||
| 711 | } | ||
| 712 | |||
| 713 | /** | ||
| 714 | * Test if can resolve a dns name. | ||
| 715 | * @param {string} name Domain name to use | ||
| 716 | * @param {boolean} useIPv6 If use IPv6 | ||
| 717 | * @param {boolean} useIPv4 If use IPv4 | ||
| 718 | * @returns Promise<boolean> If the URL is valid | ||
| 719 | */ | ||
| 720 | export async function canResolve(name, useIPv6 = true, useIPv4 = true) { | ||
| 721 | try { | ||
| 722 | let v6Resolved = false; | ||
| 723 | let v4Resolved = false; | ||
| 724 | |||
| 725 | if (useIPv6) { | ||
| 726 | try { | ||
| 727 | await dnsPromise.resolve6(name); | ||
| 728 | v6Resolved = true; | ||
| 729 | } catch (error) { | ||
| 730 | v6Resolved = false; | ||
| 731 | } | ||
| 732 | } | ||
| 733 | |||
| 734 | if (useIPv4) { | ||
| 735 | try { | ||
| 736 | await dnsPromise.resolve(name); | ||
| 737 | v4Resolved = true; | ||
| 738 | } catch (error) { | ||
| 739 | v4Resolved = false; | ||
| 740 | } | ||
| 741 | } | ||
| 742 | |||
| 743 | return v6Resolved || v4Resolved; | ||
| 744 | |||
| 745 | } catch (error) { | ||
| 746 | return false; | ||
| 747 | } | ||
| 748 | } | ||
| 749 | |||
| 750 | |||
| 751 | /** | ||
| 752 | * converts string to boolean accepts 'true' or 'false' else it returns the string put in | ||
| 753 | * @param {string|null} str Input string or null | ||
| 754 | * @returns {boolean|string|null} boolean else original input string or null if input is | ||
| 755 | */ | ||
| 756 | export function stringToBool(str) { | ||
| 757 | if (str === 'true') return true; | ||
| 758 | if (str === 'false') return false; | ||
| 759 | return str; | ||
| 760 | } | ||
| 761 | |||
| 762 | /** | ||
| 698 | * Setup the minimum log level | 763 | * Setup the minimum log level |
| 699 | */ | 764 | */ |
| 700 | export function setupLogLevel() { | 765 | export function setupLogLevel() { |