Merge pull request #3226 from BPplays/ipv6_auto Automatically detect what IP versions are available and use them
Signed| @@ -7,9 +7,11 @@ 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: auto |
| 12 | ipv6: false | 14 | ipv6: auto |
| 13 | # Prefers IPv6 for DNS. Enable this on ISPs that don't have issues with IPv6 | 15 | # Prefers IPv6 for DNS. Enable this on ISPs that don't have issues with IPv6 |
| 14 | dnsPreferIPv6: false | 16 | dnsPreferIPv6: false |
| 15 | # The hostname that autorun opens. | 17 | # The hostname that autorun opens. |
| @@ -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'; |
| @@ -67,6 +68,8 @@ import { | |||
| 67 | forwardFetchResponse, | 68 | forwardFetchResponse, |
| 68 | removeColorFormatting, | 69 | removeColorFormatting, |
| 69 | getSeparator, | 70 | getSeparator, |
| 71 | stringToBool, | ||
| 72 | urlHostnameToIPv6, | ||
| 70 | } from './src/util.js'; | 73 | } from './src/util.js'; |
| 71 | import { UPLOADS_DIRECTORY } from './src/constants.js'; | 74 | import { UPLOADS_DIRECTORY } from './src/constants.js'; |
| 72 | import { ensureThumbnailCache } from './src/endpoints/thumbnails.js'; | 75 | import { ensureThumbnailCache } from './src/endpoints/thumbnails.js'; |
| @@ -133,8 +136,8 @@ const DEFAULT_CSRF_DISABLED = false; | |||
| 133 | const DEFAULT_BASIC_AUTH = false; | 136 | const DEFAULT_BASIC_AUTH = false; |
| 134 | const DEFAULT_PER_USER_BASIC_AUTH = false; | 137 | const DEFAULT_PER_USER_BASIC_AUTH = false; |
| 135 | 138 | ||
| 136 | const DEFAULT_ENABLE_IPV6 = false; | 139 | const DEFAULT_ENABLE_IPV6 = 'auto'; |
| 137 | const DEFAULT_ENABLE_IPV4 = true; | 140 | const DEFAULT_ENABLE_IPV4 = 'auto'; |
| 138 | 141 | ||
| 139 | const DEFAULT_PREFER_IPV6 = false; | 142 | const DEFAULT_PREFER_IPV6 = false; |
| 140 | 143 | ||
| @@ -150,11 +153,11 @@ const DEFAULT_PROXY_BYPASS = []; | |||
| 150 | const cliArguments = yargs(hideBin(process.argv)) | 153 | const cliArguments = yargs(hideBin(process.argv)) |
| 151 | .usage('Usage: <your-start-script> <command> [options]') | 154 | .usage('Usage: <your-start-script> <command> [options]') |
| 152 | .option('enableIPv6', { | 155 | .option('enableIPv6', { |
| 153 | type: 'boolean', | 156 | type: 'string', |
| 154 | default: null, | 157 | default: null, |
| 155 | describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`, | 158 | describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`, |
| 156 | }).option('enableIPv4', { | 159 | }).option('enableIPv4', { |
| 157 | type: 'boolean', | 160 | type: 'string', |
| 158 | default: null, | 161 | default: null, |
| 159 | describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`, | 162 | describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`, |
| 160 | }).option('port', { | 163 | }).option('port', { |
| @@ -243,6 +246,7 @@ app.use(helmet({ | |||
| 243 | app.use(compression()); | 246 | app.use(compression()); |
| 244 | app.use(responseTime()); | 247 | app.use(responseTime()); |
| 245 | 248 | ||
| 249 | |||
| 246 | const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT); | 250 | const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT); |
| 247 | const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; | 251 | const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; |
| 248 | const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); | 252 | const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); |
| @@ -256,8 +260,9 @@ const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); | |||
| 256 | 260 | ||
| 257 | const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY); | 261 | const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY); |
| 258 | 262 | ||
| 259 | const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6); | 263 | |
| 260 | const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4); | 264 | let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6); |
| 265 | let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4); | ||
| 261 | 266 | ||
| 262 | const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME); | 267 | const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME); |
| 263 | const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT); | 268 | const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT); |
| @@ -280,7 +285,19 @@ if (dnsPreferIPv6) { | |||
| 280 | console.log('Preferring IPv4 for DNS resolution'); | 285 | console.log('Preferring IPv4 for DNS resolution'); |
| 281 | } | 286 | } |
| 282 | 287 | ||
| 283 | if (!enableIPv6 && !enableIPv4) { | 288 | |
| 289 | const ipOptions = [true, 'auto', false]; | ||
| 290 | |||
| 291 | if (!ipOptions.includes(enableIPv6)) { | ||
| 292 | console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', ipOptions, '\n setting to: auto'); | ||
| 293 | enableIPv6 = 'auto'; | ||
| 294 | } | ||
| 295 | if (!ipOptions.includes(enableIPv4)) { | ||
| 296 | console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', ipOptions, '\n setting to: auto'); | ||
| 297 | enableIPv4 = 'auto'; | ||
| 298 | } | ||
| 299 | |||
| 300 | if (enableIPv6 === false && enableIPv4 === false) { | ||
| 284 | console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.'); | 301 | console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.'); |
| 285 | process.exit(1); | 302 | process.exit(1); |
| 286 | } | 303 | } |
| @@ -365,6 +382,40 @@ function getSessionCookieAge() { | |||
| 365 | return undefined; | 382 | return undefined; |
| 366 | } | 383 | } |
| 367 | 384 | ||
| 385 | async function getHasIP() { | ||
| 386 | let hasIPv6 = false; | ||
| 387 | let hasIPv6Local = false; | ||
| 388 | |||
| 389 | let hasIPv4 = false; | ||
| 390 | let hasIPv4Local = false; | ||
| 391 | |||
| 392 | const interfaces = os.networkInterfaces(); | ||
| 393 | |||
| 394 | for (const iface of Object.values(interfaces)) { | ||
| 395 | if (iface === undefined) { | ||
| 396 | continue; | ||
| 397 | } | ||
| 398 | for (const info of iface) { | ||
| 399 | if (info.family === 'IPv6') { | ||
| 400 | hasIPv6 = true; | ||
| 401 | if (info.address === '::1') { | ||
| 402 | hasIPv6Local = true; | ||
| 403 | } | ||
| 404 | } | ||
| 405 | |||
| 406 | if (info.family === 'IPv4') { | ||
| 407 | hasIPv4 = true; | ||
| 408 | if (info.address === '127.0.0.1') { | ||
| 409 | hasIPv4Local = true; | ||
| 410 | } | ||
| 411 | } | ||
| 412 | if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break; | ||
| 413 | } | ||
| 414 | if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break; | ||
| 415 | } | ||
| 416 | return [hasIPv6, hasIPv4, hasIPv6Local, hasIPv4Local]; | ||
| 417 | } | ||
| 418 | |||
| 368 | app.use(cookieSession({ | 419 | app.use(cookieSession({ |
| 369 | name: getCookieSessionName(), | 420 | name: getCookieSessionName(), |
| 370 | sameSite: 'strict', | 421 | sameSite: 'strict', |
| @@ -685,18 +736,18 @@ const preSetupTasks = async function () { | |||
| 685 | * Gets the hostname to use for autorun in the browser. | 736 | * Gets the hostname to use for autorun in the browser. |
| 686 | * @returns {string} The hostname to use for autorun | 737 | * @returns {string} The hostname to use for autorun |
| 687 | */ | 738 | */ |
| 688 | function getAutorunHostname() { | 739 | function getAutorunHostname(useIPv6, useIPv4) { |
| 689 | if (autorunHostname === 'auto') { | 740 | if (autorunHostname === 'auto') { |
| 690 | if (enableIPv6 && enableIPv4) { | 741 | if (useIPv6 && useIPv4) { |
| 691 | if (avoidLocalhost) return '[::1]'; | 742 | if (avoidLocalhost) return '[::1]'; |
| 692 | return 'localhost'; | 743 | return 'localhost'; |
| 693 | } | 744 | } |
| 694 | 745 | ||
| 695 | if (enableIPv6) { | 746 | if (useIPv6) { |
| 696 | return '[::1]'; | 747 | return '[::1]'; |
| 697 | } | 748 | } |
| 698 | 749 | ||
| 699 | if (enableIPv4) { | 750 | if (useIPv4) { |
| 700 | return '127.0.0.1'; | 751 | return '127.0.0.1'; |
| 701 | } | 752 | } |
| 702 | } | 753 | } |
| @@ -709,10 +760,10 @@ function getAutorunHostname() { | |||
| 709 | * @param {boolean} v6Failed If the server failed to start on IPv6 | 760 | * @param {boolean} v6Failed If the server failed to start on IPv6 |
| 710 | * @param {boolean} v4Failed If the server failed to start on IPv4 | 761 | * @param {boolean} v4Failed If the server failed to start on IPv4 |
| 711 | */ | 762 | */ |
| 712 | const postSetupTasks = async function (v6Failed, v4Failed) { | 763 | const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { |
| 713 | const autorunUrl = new URL( | 764 | const autorunUrl = new URL( |
| 714 | (cliArguments.ssl ? 'https://' : 'http://') + | 765 | (cliArguments.ssl ? 'https://' : 'http://') + |
| 715 | (getAutorunHostname()) + | 766 | (getAutorunHostname(useIPv6, useIPv4)) + |
| 716 | (':') + | 767 | (':') + |
| 717 | ((autorunPortOverride >= 0) ? autorunPortOverride : server_port), | 768 | ((autorunPortOverride >= 0) ? autorunPortOverride : server_port), |
| 718 | ); | 769 | ); |
| @@ -725,11 +776,11 @@ const postSetupTasks = async function (v6Failed, v4Failed) { | |||
| 725 | 776 | ||
| 726 | let logListen = 'SillyTavern is listening on'; | 777 | let logListen = 'SillyTavern is listening on'; |
| 727 | 778 | ||
| 728 | if (enableIPv6 && !v6Failed) { | 779 | if (useIPv6 && !v6Failed) { |
| 729 | logListen += color.green(' IPv6: ' + tavernUrlV6.host); | 780 | logListen += color.green(' IPv6: ' + tavernUrlV6.host); |
| 730 | } | 781 | } |
| 731 | 782 | ||
| 732 | if (enableIPv4 && !v4Failed) { | 783 | if (useIPv4 && !v4Failed) { |
| 733 | logListen += color.green(' IPv4: ' + tavernUrl.host); | 784 | logListen += color.green(' IPv4: ' + tavernUrl.host); |
| 734 | } | 785 | } |
| 735 | 786 | ||
| @@ -805,13 +856,13 @@ function logSecurityAlert(message) { | |||
| 805 | * @param {boolean} v6Failed If the server failed to start on IPv6 | 856 | * @param {boolean} v6Failed If the server failed to start on IPv6 |
| 806 | * @param {boolean} v4Failed If the server failed to start on IPv4 | 857 | * @param {boolean} v4Failed If the server failed to start on IPv4 |
| 807 | */ | 858 | */ |
| 808 | function handleServerListenFail(v6Failed, v4Failed) { | 859 | function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) { |
| 809 | if (v6Failed && !enableIPv4) { | 860 | if (v6Failed && !useIPv4) { |
| 810 | console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled')); | 861 | console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled')); |
| 811 | process.exit(1); | 862 | process.exit(1); |
| 812 | } | 863 | } |
| 813 | 864 | ||
| 814 | if (v4Failed && !enableIPv6) { | 865 | if (v4Failed && !useIPv6) { |
| 815 | console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled')); | 866 | console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled')); |
| 816 | process.exit(1); | 867 | process.exit(1); |
| 817 | } | 868 | } |
| @@ -828,7 +879,7 @@ function handleServerListenFail(v6Failed, v4Failed) { | |||
| 828 | * @returns {Promise<void>} A promise that resolves when the server is listening | 879 | * @returns {Promise<void>} A promise that resolves when the server is listening |
| 829 | * @throws {Error} If the server fails to start | 880 | * @throws {Error} If the server fails to start |
| 830 | */ | 881 | */ |
| 831 | function createHttpsServer(url) { | 882 | function createHttpsServer(url, ipVersion) { |
| 832 | return new Promise((resolve, reject) => { | 883 | return new Promise((resolve, reject) => { |
| 833 | const server = https.createServer( | 884 | const server = https.createServer( |
| 834 | { | 885 | { |
| @@ -837,7 +888,15 @@ function createHttpsServer(url) { | |||
| 837 | }, app); | 888 | }, app); |
| 838 | server.on('error', reject); | 889 | server.on('error', reject); |
| 839 | server.on('listening', resolve); | 890 | server.on('listening', resolve); |
| 840 | server.listen(Number(url.port || 443), url.hostname); | 891 | |
| 892 | let host = url.hostname; | ||
| 893 | if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname); | ||
| 894 | server.listen({ | ||
| 895 | host: host, | ||
| 896 | port: Number(url.port || 443), | ||
| 897 | // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used | ||
| 898 | ipv6Only: true, | ||
| 899 | }); | ||
| 841 | }); | 900 | }); |
| 842 | } | 901 | } |
| 843 | 902 | ||
| @@ -847,24 +906,32 @@ function createHttpsServer(url) { | |||
| 847 | * @returns {Promise<void>} A promise that resolves when the server is listening | 906 | * @returns {Promise<void>} A promise that resolves when the server is listening |
| 848 | * @throws {Error} If the server fails to start | 907 | * @throws {Error} If the server fails to start |
| 849 | */ | 908 | */ |
| 850 | function createHttpServer(url) { | 909 | function createHttpServer(url, ipVersion) { |
| 851 | return new Promise((resolve, reject) => { | 910 | return new Promise((resolve, reject) => { |
| 852 | const server = http.createServer(app); | 911 | const server = http.createServer(app); |
| 853 | server.on('error', reject); | 912 | server.on('error', reject); |
| 854 | server.on('listening', resolve); | 913 | server.on('listening', resolve); |
| 855 | server.listen(Number(url.port || 80), url.hostname); | 914 | |
| 915 | let host = url.hostname; | ||
| 916 | if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname); | ||
| 917 | server.listen({ | ||
| 918 | host: host, | ||
| 919 | port: Number(url.port || 80), | ||
| 920 | // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used | ||
| 921 | ipv6Only: true, | ||
| 922 | }); | ||
| 856 | }); | 923 | }); |
| 857 | } | 924 | } |
| 858 | 925 | ||
| 859 | async function startHTTPorHTTPS() { | 926 | async function startHTTPorHTTPS(useIPv6, useIPv4) { |
| 860 | let v6Failed = false; | 927 | let v6Failed = false; |
| 861 | let v4Failed = false; | 928 | let v4Failed = false; |
| 862 | 929 | ||
| 863 | const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer; | 930 | const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer; |
| 864 | 931 | ||
| 865 | if (enableIPv6) { | 932 | if (useIPv6) { |
| 866 | try { | 933 | try { |
| 867 | await createFunc(tavernUrlV6); | 934 | await createFunc(tavernUrlV6, 6); |
| 868 | } catch (error) { | 935 | } catch (error) { |
| 869 | console.error('non-fatal error: failed to start server on IPv6'); | 936 | console.error('non-fatal error: failed to start server on IPv6'); |
| 870 | console.error(error); | 937 | console.error(error); |
| @@ -873,9 +940,9 @@ async function startHTTPorHTTPS() { | |||
| 873 | } | 940 | } |
| 874 | } | 941 | } |
| 875 | 942 | ||
| 876 | if (enableIPv4) { | 943 | if (useIPv4) { |
| 877 | try { | 944 | try { |
| 878 | await createFunc(tavernUrl); | 945 | await createFunc(tavernUrl, 4); |
| 879 | } catch (error) { | 946 | } catch (error) { |
| 880 | console.error('non-fatal error: failed to start server on IPv4'); | 947 | console.error('non-fatal error: failed to start server on IPv4'); |
| 881 | console.error(error); | 948 | console.error(error); |
| @@ -888,10 +955,60 @@ async function startHTTPorHTTPS() { | |||
| 888 | } | 955 | } |
| 889 | 956 | ||
| 890 | async function startServer() { | 957 | async function startServer() { |
| 891 | const [v6Failed, v4Failed] = await startHTTPorHTTPS(); | 958 | let useIPv6 = (enableIPv6 === true); |
| 959 | let useIPv4 = (enableIPv4 === true); | ||
| 960 | let hasIPv6, hasIPv4, hasIPv6Local, hasIPv4Local, hasIPv6Any, hasIPv4Any; | ||
| 961 | |||
| 962 | |||
| 963 | if (enableIPv6 === 'auto' || enableIPv4 === 'auto') { | ||
| 964 | [hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local] = await getHasIP(); | ||
| 965 | |||
| 966 | hasIPv6 = listen ? hasIPv6Any : hasIPv6Local; | ||
| 967 | if (enableIPv6 === 'auto') { | ||
| 968 | useIPv6 = hasIPv6; | ||
| 969 | } | ||
| 970 | if (hasIPv6) { | ||
| 971 | if (useIPv6) { | ||
| 972 | console.log(color.green('IPv6 support detected')); | ||
| 973 | } else { | ||
| 974 | console.log('IPv6 support detected (but disabled)'); | ||
| 975 | } | ||
| 976 | } | ||
| 977 | |||
| 978 | |||
| 979 | hasIPv4 = listen ? hasIPv4Any : hasIPv4Local; | ||
| 980 | if (enableIPv4 === 'auto') { | ||
| 981 | useIPv4 = hasIPv4; | ||
| 982 | } | ||
| 983 | if (hasIPv4) { | ||
| 984 | if (useIPv4) { | ||
| 985 | console.log(color.green('IPv4 support detected')); | ||
| 986 | } else { | ||
| 987 | console.log('IPv4 support detected (but disabled)'); | ||
| 988 | } | ||
| 989 | } | ||
| 990 | } else { | ||
| 991 | console.log("Neither protocol: ipv6, nor ipv4 are set to auto, skipping detection") | ||
| 992 | } | ||
| 993 | |||
| 994 | |||
| 995 | |||
| 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 | } | ||
| 1002 | |||
| 1003 | 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'); | ||
| 1005 | process.exit(1); | ||
| 1006 | } | ||
| 1007 | |||
| 1008 | const [v6Failed, v4Failed] = await startHTTPorHTTPS(useIPv6, useIPv4); | ||
| 892 | 1009 | ||
| 893 | handleServerListenFail(v6Failed, v4Failed); | 1010 | handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4); |
| 894 | postSetupTasks(v6Failed, v4Failed); | 1011 | postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4); |
| 895 | } | 1012 | } |
| 896 | 1013 | ||
| 897 | async function verifySecuritySettings() { | 1014 | async function verifySecuritySettings() { |
| @@ -692,6 +692,22 @@ export function isValidUrl(url) { | |||
| 692 | } | 692 | } |
| 693 | } | 693 | } |
| 694 | 694 | ||
| 695 | export function urlHostnameToIPv6(hostname) { | ||
| 696 | if (hostname.startsWith('[')) { | ||
| 697 | hostname = hostname.slice(1); | ||
| 698 | } | ||
| 699 | if (hostname.endsWith(']')) { | ||
| 700 | hostname = hostname.slice(0, -1); | ||
| 701 | } | ||
| 702 | return hostname; | ||
| 703 | } | ||
| 704 | |||
| 705 | export function stringToBool(str) { | ||
| 706 | if (str === 'true') return true; | ||
| 707 | if (str === 'false') return false; | ||
| 708 | return str; | ||
| 709 | } | ||
| 710 | |||
| 695 | /** | 711 | /** |
| 696 | * MemoryLimitedMap class that limits the memory usage of string values. | 712 | * MemoryLimitedMap class that limits the memory usage of string values. |
| 697 | */ | 713 | */ |