From dd7391caafe13a3b2ddc55ab501450ee6d0d6911 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 13 Feb 2025 20:17:33 +0200 Subject: [PATCH 01/22] Ollama: Add num_batch config value --- default/config.yaml | 4 ++++ src/constants.js | 1 + src/endpoints/backends/text-completions.js | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/default/config.yaml b/default/config.yaml index 94673ca11..5c885422c 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -183,6 +183,10 @@ ollama: # * 0: Unload the model immediately after the request # * N (any positive number): Keep the model loaded for N seconds after the request. keepAlive: -1 + # Controls the "num_batch" (batch size) parameter of the generation request + # * -1: Use the default value of the model + # * N (positive number): Use the specified value. Must be a power of 2, e.g. 128, 256, 512, etc. + batchSize: -1 # -- ANTHROPIC CLAUDE API CONFIGURATION -- claude: # Enables caching of the system prompt (if supported). diff --git a/src/constants.js b/src/constants.js index 30f6f2da0..66697fed4 100644 --- a/src/constants.js +++ b/src/constants.js @@ -304,6 +304,7 @@ export const TOGETHERAI_KEYS = [ export const OLLAMA_KEYS = [ 'num_predict', 'num_ctx', + 'num_batch', 'stop', 'temperature', 'repeat_penalty', diff --git a/src/endpoints/backends/text-completions.js b/src/endpoints/backends/text-completions.js index e58474ed4..c5da6280d 100644 --- a/src/endpoints/backends/text-completions.js +++ b/src/endpoints/backends/text-completions.js @@ -373,6 +373,10 @@ router.post('/generate', jsonParser, async function (request, response) { if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) { const keepAlive = getConfigValue('ollama.keepAlive', -1); + const numBatch = getConfigValue('ollama.batchSize', -1); + if (numBatch > 0) { + request.body['num_batch'] = numBatch; + } args.body = JSON.stringify({ model: request.body.model, prompt: request.body.prompt, From 6e0ed8552ff01f1ce445d5d62c2b261fababd0f8 Mon Sep 17 00:00:00 2001 From: Kristan Schlikow Date: Thu, 13 Feb 2025 19:34:34 +0100 Subject: [PATCH 02/22] Add support for FAL.AI as image gen provider --- .../extensions/stable-diffusion/index.js | 65 ++++++++++ .../extensions/stable-diffusion/settings.html | 15 +++ public/scripts/secrets.js | 1 + src/endpoints/secrets.js | 1 + src/endpoints/stable-diffusion.js | 120 ++++++++++++++++++ 5 files changed, 202 insertions(+) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 9f9960c7d..6d410a309 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -81,6 +81,7 @@ const sources = { huggingface: 'huggingface', nanogpt: 'nanogpt', bfl: 'bfl', + falai: 'falai', }; const initiators = { @@ -1169,6 +1170,10 @@ async function onBflKeyClick() { return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL); } +async function onFalaiKeyClick() { + return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI); +} + function onBflUpsamplingInput() { extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked'); saveSettingsDebounced(); @@ -1707,6 +1712,9 @@ async function loadModels() { case sources.bfl: models = await loadBflModels(); break; + case sources.falai: + models = await loadFalaiModels(); + break; } for (const model of models) { @@ -1744,6 +1752,21 @@ async function loadBflModels() { ]; } +async function loadFalaiModels() { + $('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]); + + const result = await fetch('/api/sd/falai/models', { + method: 'POST', + headers: getRequestHeaders(), + }); + + if (result.ok) { + return await result.json(); + } + + return []; +} + async function loadPollinationsModels() { const result = await fetch('/api/sd/pollinations/models', { method: 'POST', @@ -2081,6 +2104,9 @@ async function loadSchedulers() { case sources.bfl: schedulers = ['N/A']; break; + case sources.falai: + schedulers = ['N/A']; + break; } for (const scheduler of schedulers) { @@ -2735,6 +2761,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP case sources.bfl: result = await generateBflImage(prefixedPrompt, signal); break; + case sources.falai: + result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal); + break; } if (!result.data) { @@ -3496,6 +3525,39 @@ async function generateBflImage(prompt, signal) { } } +/** + * Generates an image using the FAL.AI API. + * @param {string} prompt - The main instruction used to guide the image generation. + * @param {string} negativePrompt - The negative prompt used to guide the image generation. + * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. + */ +async function generateFalaiImage(prompt, negativePrompt, signal) { + const result = await fetch('/api/sd/falai/generate', { + method: 'POST', + headers: getRequestHeaders(), + signal: signal, + body: JSON.stringify({ + prompt: prompt, + negative_prompt: negativePrompt, + model: extension_settings.sd.model, + steps: clamp(extension_settings.sd.steps, 1, 50), + guidance: clamp(extension_settings.sd.scale, 1.5, 5), + width: clamp(extension_settings.sd.width, 256, 1440), + height: clamp(extension_settings.sd.height, 256, 1440), + seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, + }), + }); + + if (result.ok) { + const data = await result.json(); + return { format: 'jpg', data: data.image }; + } else { + const text = await result.text(); + throw new Error(text); + } +} + async function onComfyOpenWorkflowEditorClick() { let workflow = await (await fetch('/api/sd/comfy/workflow', { method: 'POST', @@ -3782,6 +3844,8 @@ function isValidState() { return secret_state[SECRET_KEYS.NANOGPT]; case sources.bfl: return secret_state[SECRET_KEYS.BFL]; + case sources.falai: + return secret_state[SECRET_KEYS.FALAI]; } } @@ -4443,6 +4507,7 @@ jQuery(async () => { $('#sd_function_tool').on('input', onFunctionToolInput); $('#sd_bfl_key').on('click', onBflKeyClick); $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput); + $('#sd_falai_key').on('click', onFalaiKeyClick); if (!CSS.supports('field-sizing', 'content')) { $('.sd_settings .inline-drawer-toggle').on('click', function () { diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index 7969ef7e2..7f766d8cd 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -52,6 +52,7 @@ +
@@ -256,6 +257,20 @@
+
+
+ + API Key + + + + +
+
+
diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index f2e27f3e2..261c31480 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -41,6 +41,7 @@ export const SECRET_KEYS = { GENERIC: 'api_key_generic', DEEPSEEK: 'api_key_deepseek', SERPER: 'api_key_serper', + FALAI: 'api_key_falai', }; const INPUT_MAP = { diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index 5683da4d8..a7f0094ba 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -50,6 +50,7 @@ export const SECRET_KEYS = { TAVILY: 'api_key_tavily', NANOGPT: 'api_key_nanogpt', BFL: 'api_key_bfl', + FALAI: 'api_key_falai', GENERIC: 'api_key_generic', DEEPSEEK: 'api_key_deepseek', SERPER: 'api_key_serper', diff --git a/src/endpoints/stable-diffusion.js b/src/endpoints/stable-diffusion.js index 2d611dc6b..2c23bab98 100644 --- a/src/endpoints/stable-diffusion.js +++ b/src/endpoints/stable-diffusion.js @@ -1228,6 +1228,125 @@ bfl.post('/generate', jsonParser, async (request, response) => { } }); +const falai = express.Router(); + +falai.post('/models', jsonParser, async (_request, response) => { + try { + const modelsUrl = new URL('https://fal.ai/api/models?categories=text-to-image'); + const result = await fetch(modelsUrl); + + if (!result.ok) { + console.warn('FAL.AI returned an error.', result.status, result.statusText); + throw new Error('FAL.AI request failed.'); + } + + const data = await result.json(); + + if (!Array.isArray(data)) { + console.warn('FAL.AI returned invalid data.'); + throw new Error('FAL.AI request failed.'); + } + + const models = data + .filter(x => !x.title.toLowerCase().includes('inpainting') && + !x.title.toLowerCase().includes('control') && + !x.title.toLowerCase().includes('upscale')) + .map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title })); + return response.send(models); + } catch (error) { + console.error(error); + return response.sendStatus(500); + } +}); + +falai.post('/generate', jsonParser, async (request, response) => { + try { + const key = readSecret(request.user.directories, SECRET_KEYS.FALAI); + + if (!key) { + console.warn('FAL.AI key not found.'); + return response.sendStatus(400); + } + + const requestBody = { + prompt: request.body.prompt, + image_size: { 'width': request.body.width, 'height': request.body.height }, + num_inference_steps: request.body.steps, + seed: request.body.seed ?? null, + guidance_scale: request.body.guidance, + enable_safety_checker: false, + }; + + console.debug('FAL.AI request:', requestBody); + + const result = await fetch(`https://queue.fal.run/fal-ai/${request.body.model}`, { + method: 'POST', + body: JSON.stringify(requestBody), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Key ${key}`, + }, + }); + + if (!result.ok) { + console.warn('FAL.AI returned an error.'); + return response.sendStatus(500); + } + + /** @type {any} */ + const taskData = await result.json(); + const { status_url } = taskData; + + const MAX_ATTEMPTS = 100; + for (let i = 0; i < MAX_ATTEMPTS; i++) { + await delay(2500); + + const statusResult = await fetch(status_url, { + headers: { + 'Authorization': `Key ${key}`, + }, + }); + + if (!statusResult.ok) { + const text = await statusResult.text(); + console.warn('FAL.AI returned an error.', text); + return response.sendStatus(500); + } + + /** @type {any} */ + const statusData = await statusResult.json(); + + if (statusData?.status === 'IN_QUEUE' || statusData?.status === 'IN_PROGRESS') { + continue; + } + + if (statusData?.status === 'COMPLETED') { + const resultFetch = await fetch(statusData?.response_url, { + method: 'GET', + headers: { + 'Authorization': `Key ${key}`, + }, + }); + const resultData = await resultFetch.json(); + const imageFetch = await fetch(resultData?.images[0].url, { + headers: { + 'Authorization': `Key ${key}`, + }, + }); + + const fetchData = await imageFetch.arrayBuffer(); + const image = Buffer.from(fetchData).toString('base64'); + return response.send({ image: image }); + } + + throw new Error('FAL.AI failed to generate image.', { cause: statusData }); + } + } catch (error) { + console.error(error); + return response.sendStatus(500); + } +}); + router.use('/comfy', comfy); router.use('/together', together); router.use('/drawthings', drawthings); @@ -1237,3 +1356,4 @@ router.use('/blockentropy', blockentropy); router.use('/huggingface', huggingface); router.use('/nanogpt', nanogpt); router.use('/bfl', bfl); +router.use('/falai', falai); From b033b98532b690ac7cc96844fc3ec229be3dde76 Mon Sep 17 00:00:00 2001 From: Kristan Schlikow Date: Thu, 13 Feb 2025 21:09:13 +0100 Subject: [PATCH 03/22] Address issues raised in PR --- public/scripts/extensions/stable-diffusion/index.js | 1 + public/scripts/extensions/stable-diffusion/settings.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 6d410a309..74378396e 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -1304,6 +1304,7 @@ async function onModelChange() { sources.huggingface, sources.nanogpt, sources.bfl, + sources.falai, ]; if (cloudSources.includes(extension_settings.sd.source)) { diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index 7f766d8cd..32ecfe28f 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -42,6 +42,7 @@ + @@ -52,7 +53,6 @@ -
From 76becb43ae0fbbf4842fbc1ea5104ac851de5d7c Mon Sep 17 00:00:00 2001 From: Kristan Schlikow Date: Thu, 13 Feb 2025 21:43:08 +0100 Subject: [PATCH 04/22] Pass through errors coming from FAL to the user --- public/scripts/extensions/stable-diffusion/index.js | 1 + src/endpoints/stable-diffusion.js | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 74378396e..58429d4fd 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -3555,6 +3555,7 @@ async function generateFalaiImage(prompt, negativePrompt, signal) { return { format: 'jpg', data: data.image }; } else { const text = await result.text(); + console.log(text); throw new Error(text); } } diff --git a/src/endpoints/stable-diffusion.js b/src/endpoints/stable-diffusion.js index 2c23bab98..dd6dc9adf 100644 --- a/src/endpoints/stable-diffusion.js +++ b/src/endpoints/stable-diffusion.js @@ -1251,6 +1251,7 @@ falai.post('/models', jsonParser, async (_request, response) => { .filter(x => !x.title.toLowerCase().includes('inpainting') && !x.title.toLowerCase().includes('control') && !x.title.toLowerCase().includes('upscale')) + .sort((a, b) => a.title.localeCompare(b.title)) .map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title })); return response.send(models); } catch (error) { @@ -1328,6 +1329,11 @@ falai.post('/generate', jsonParser, async (request, response) => { }, }); const resultData = await resultFetch.json(); + + if (resultData.detail !== null && resultData.detail !== undefined) { + throw new Error('FAL.AI failed to generate image.', { cause: `${resultData.detail[0].loc[1]}: ${resultData.detail[0].msg}` }); + } + const imageFetch = await fetch(resultData?.images[0].url, { headers: { 'Authorization': `Key ${key}`, @@ -1343,7 +1349,7 @@ falai.post('/generate', jsonParser, async (request, response) => { } } catch (error) { console.error(error); - return response.sendStatus(500); + return response.status(500).send(error.cause || error.message); } }); From 09aaa9181c6eaa55d5f65a342ec90438c594fd2b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 13 Feb 2025 23:34:20 +0200 Subject: [PATCH 05/22] Good night 'Black Magic Time' --- default/content/index.json | 4 -- .../presets/moving-ui/Black Magic Time.json | 45 ------------------- 2 files changed, 49 deletions(-) delete mode 100644 default/content/presets/moving-ui/Black Magic Time.json diff --git a/default/content/index.json b/default/content/index.json index 82387d0a9..4caa21c14 100644 --- a/default/content/index.json +++ b/default/content/index.json @@ -671,10 +671,6 @@ "filename": "presets/moving-ui/Default.json", "type": "moving_ui" }, - { - "filename": "presets/moving-ui/Black Magic Time.json", - "type": "moving_ui" - }, { "filename": "presets/quick-replies/Default.json", "type": "quick_replies" diff --git a/default/content/presets/moving-ui/Black Magic Time.json b/default/content/presets/moving-ui/Black Magic Time.json deleted file mode 100644 index 72b9539ca..000000000 --- a/default/content/presets/moving-ui/Black Magic Time.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "Black Magic Time", - "movingUIState": { - "sheld": { - "top": 488, - "left": 1407, - "right": 1, - "bottom": 4, - "margin": "unset", - "width": 471, - "height": 439 - }, - "floatingPrompt": { - "width": 369, - "height": 441 - }, - "right-nav-panel": { - "top": 0, - "left": 1400, - "right": 111, - "bottom": 446, - "margin": "unset", - "width": 479, - "height": 487 - }, - "WorldInfo": { - "top": 41, - "left": 369, - "right": 642, - "bottom": 51, - "margin": "unset", - "width": 1034, - "height": 858 - }, - "left-nav-panel": { - "top": 442, - "left": 0, - "right": 1546, - "bottom": 25, - "margin": "unset", - "width": 368, - "height": 483 - } - } -} \ No newline at end of file From 83f74a5d225ce8aabb469e4ff8f11e64c9791e75 Mon Sep 17 00:00:00 2001 From: Kristan Schlikow Date: Thu, 13 Feb 2025 23:43:10 +0100 Subject: [PATCH 06/22] Allow user to configure an address to listen to --- default/config.yaml | 2 ++ server.js | 44 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/default/config.yaml b/default/config.yaml index 94673ca11..813820035 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -6,6 +6,8 @@ cardsCacheCapacity: 100 # -- SERVER CONFIGURATION -- # Listen for incoming connections listen: false +# Listen on a specific address, supports IPv4 and IPv6 +listenAddress: 127.0.0.1 # Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled! # - Use option "auto" to automatically detect support # - Use true or false (no qoutes) to enable or disable each protocol diff --git a/server.js b/server.js index e62562e56..12b72bc69 100644 --- a/server.js +++ b/server.js @@ -130,6 +130,7 @@ if (process.versions && process.versions.node && process.versions.node.match(/20 const DEFAULT_PORT = 8000; const DEFAULT_AUTORUN = false; const DEFAULT_LISTEN = false; +const DEFAULT_LISTEN_ADDRESS = ''; const DEFAULT_CORS_PROXY = false; const DEFAULT_WHITELIST = true; const DEFAULT_ACCOUNTS = false; @@ -185,6 +186,10 @@ const cliArguments = yargs(hideBin(process.argv)) type: 'boolean', default: null, describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`, + }).option('listenAddress', { + type: 'string', + default: null, + describe: 'Set SillyTavern to listen to a specific address. If not set, it will fallback to listen to all.\n[config default: empty ]', }).option('corsProxy', { type: 'boolean', default: null, @@ -254,6 +259,8 @@ const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getCon const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; /** @type {boolean} */ const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); +/** @type {string} */ +const listenAddress = cliArguments.listenAddress ?? getConfigValue('listenAddress', DEFAULT_LISTEN_ADDRESS); /** @type {boolean} */ const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); @@ -706,15 +713,17 @@ app.use('/api/backends/scale-alt', scaleAltRouter); app.use('/api/speech', speechRouter); app.use('/api/azure', azureRouter); +const ipv6_regex = /^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$/m; const tavernUrlV6 = new URL( (cliArguments.ssl ? 'https://' : 'http://') + - (listen ? '[::]' : '[::1]') + + (listen ? (ipv6_regex.test(listenAddress) ? listenAddress : '[::]') : '[::1]') + (':' + server_port), ); +const ipv4_regex = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/m; const tavernUrl = new URL( (cliArguments.ssl ? 'https://' : 'http://') + - (listen ? '0.0.0.0' : '127.0.0.1') + + (listen ? (ipv4_regex.test(listenAddress) ? listenAddress : '0.0.0.0') : '127.0.0.1') + (':' + server_port), ); @@ -780,6 +789,10 @@ const preSetupTasks = async function () { */ async function getAutorunHostname(useIPv6, useIPv4) { if (autorunHostname === 'auto') { + if (listen && (ipv4_regex.test(listenAddress) || ipv6_regex.test(listenAddress))) { + return listenAddress; + } + let localhostResolve = await canResolve('localhost', useIPv6, useIPv4); if (useIPv6 && useIPv4) { @@ -842,9 +855,15 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); if (listen) { - 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', - ); + if (ipv4_regex.test(listenAddress) || ipv6_regex.test(listenAddress)) { + console.log( + `SillyTavern is listening on the address ${listenAddress}. 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`, + ); + } else { + 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', + ); + } } if (basicAuthMode) { @@ -908,6 +927,19 @@ function logSecurityAlert(message) { process.exit(1); } +/** + * Prints a warning message + * @param {string} message The warning message to print + * @returns {void} + */ +function logSecurityWarning(message) { + if (basicAuthMode || enableWhitelist) return; // safe! + console.error(color.yellow(message)); + if (getConfigValue('securityOverride', false)) { + console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.')); + } +} + /** * Handles the case where the server failed to start on one or both protocols. * @param {boolean} v6Failed If the server failed to start on IPv6 @@ -1083,7 +1115,7 @@ async function verifySecuritySettings() { } if (!enableAccounts) { - logSecurityAlert('Your SillyTavern is currently insecurely open to the public. Enable whitelisting, basic authentication or user accounts.'); + logSecurityAlert('Your current SillyTavern configuration is insecure (listening to non-localhost). Enable whitelisting, basic authentication or user accounts.'); } const users = await getAllEnabledUsers(); From dd55b2770a8203ebfa52f76cb5a963e69be37127 Mon Sep 17 00:00:00 2001 From: Kristan Schlikow Date: Thu, 13 Feb 2025 23:51:18 +0100 Subject: [PATCH 07/22] Address issues with IPv6 binding --- server.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 12b72bc69..5a87d77c9 100644 --- a/server.js +++ b/server.js @@ -716,7 +716,7 @@ app.use('/api/azure', azureRouter); const ipv6_regex = /^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$/m; const tavernUrlV6 = new URL( (cliArguments.ssl ? 'https://' : 'http://') + - (listen ? (ipv6_regex.test(listenAddress) ? listenAddress : '[::]') : '[::1]') + + (listen ? (ipv6_regex.test(listenAddress) ? `[${listenAddress}]` : '[::]') : '[::1]') + (':' + server_port), ); @@ -789,8 +789,12 @@ const preSetupTasks = async function () { */ async function getAutorunHostname(useIPv6, useIPv4) { if (autorunHostname === 'auto') { - if (listen && (ipv4_regex.test(listenAddress) || ipv6_regex.test(listenAddress))) { - return listenAddress; + if (listen) { + if (ipv4_regex.test(listenAddress)) { + return listenAddress; + } else if (ipv6_regex.test(listenAddress)) { + return `[${listenAddress}]`; + } } let localhostResolve = await canResolve('localhost', useIPv6, useIPv4); @@ -855,10 +859,14 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); if (listen) { - if (ipv4_regex.test(listenAddress) || ipv6_regex.test(listenAddress)) { + if (ipv4_regex.test(listenAddress)) { console.log( `SillyTavern is listening on the address ${listenAddress}. 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`, ); + } else if (ipv6_regex.test(listenAddress)) { + console.log( + `SillyTavern is listening on the address [${listenAddress}]. 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`, + ); } else { 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', From ad8f0f564f238937f02933eda2c0f78377734536 Mon Sep 17 00:00:00 2001 From: Kristan Schlikow Date: Fri, 14 Feb 2025 19:06:07 +0100 Subject: [PATCH 08/22] Use IP Regex package, update default --- default/config.yaml | 2 +- package-lock.json | 13 +++++++++++++ package.json | 1 + server.js | 15 +++++++-------- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/default/config.yaml b/default/config.yaml index 813820035..9c37e9bf0 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -7,7 +7,7 @@ cardsCacheCapacity: 100 # Listen for incoming connections listen: false # Listen on a specific address, supports IPv4 and IPv6 -listenAddress: 127.0.0.1 +listenAddress: 0.0.0.0 # Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled! # - Use option "auto" to automatically detect support # - Use true or false (no qoutes) to enable or disable each protocol diff --git a/package-lock.json b/package-lock.json index ddbffd0f1..3c5615536 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "html-entities": "^2.5.2", "iconv-lite": "^0.6.3", "ip-matching": "^2.1.2", + "ip-regex": "^5.0.0", "ipaddr.js": "^2.0.1", "jimp": "^0.22.10", "localforage": "^1.10.0", @@ -4628,6 +4629,18 @@ "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==", "license": "LGPL-3.0-only" }, + "node_modules/ip-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", diff --git a/package.json b/package.json index 7a29b2c6c..6f687a384 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "html-entities": "^2.5.2", "iconv-lite": "^0.6.3", "ip-matching": "^2.1.2", + "ip-regex": "^5.0.0", "ipaddr.js": "^2.0.1", "jimp": "^0.22.10", "localforage": "^1.10.0", diff --git a/server.js b/server.js index 5a87d77c9..ac146bc2d 100644 --- a/server.js +++ b/server.js @@ -30,6 +30,7 @@ import bodyParser from 'body-parser'; // net related library imports import fetch from 'node-fetch'; +import ipRegex from 'ip-regex'; // Unrestrict console logs display limit util.inspect.defaultOptions.maxArrayLength = null; @@ -713,17 +714,15 @@ app.use('/api/backends/scale-alt', scaleAltRouter); app.use('/api/speech', speechRouter); app.use('/api/azure', azureRouter); -const ipv6_regex = /^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$/m; const tavernUrlV6 = new URL( (cliArguments.ssl ? 'https://' : 'http://') + - (listen ? (ipv6_regex.test(listenAddress) ? `[${listenAddress}]` : '[::]') : '[::1]') + + (listen ? (ipRegex.v6({ exact: true }).test(listenAddress) ? `[${listenAddress}]` : '[::]') : '[::1]') + (':' + server_port), ); -const ipv4_regex = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/m; const tavernUrl = new URL( (cliArguments.ssl ? 'https://' : 'http://') + - (listen ? (ipv4_regex.test(listenAddress) ? listenAddress : '0.0.0.0') : '127.0.0.1') + + (listen ? (ipRegex.v4({ exact: true }).test(listenAddress) ? listenAddress : '0.0.0.0') : '127.0.0.1') + (':' + server_port), ); @@ -790,9 +789,9 @@ const preSetupTasks = async function () { async function getAutorunHostname(useIPv6, useIPv4) { if (autorunHostname === 'auto') { if (listen) { - if (ipv4_regex.test(listenAddress)) { + if (ipRegex.v4({ exact: true }).test(listenAddress)) { return listenAddress; - } else if (ipv6_regex.test(listenAddress)) { + } else if (ipRegex.v6({ exact: true }).test(listenAddress)) { return `[${listenAddress}]`; } } @@ -859,11 +858,11 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); if (listen) { - if (ipv4_regex.test(listenAddress)) { + if (ipRegex.v4({ exact: true }).test(listenAddress)) { console.log( `SillyTavern is listening on the address ${listenAddress}. 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`, ); - } else if (ipv6_regex.test(listenAddress)) { + } else if (ipRegex.v6({ exact: true }).test(listenAddress)) { console.log( `SillyTavern is listening on the address [${listenAddress}]. 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`, ); From 2445b6d9dc11fe8291d4db773d0b3f7233c1f4be Mon Sep 17 00:00:00 2001 From: Kristan Schlikow Date: Fri, 14 Feb 2025 19:58:59 +0100 Subject: [PATCH 09/22] Split up listen address configuration between IPv4 and IPv6 --- default/config.yaml | 3 ++- server.js | 35 +++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/default/config.yaml b/default/config.yaml index 9c37e9bf0..c84251b16 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -7,7 +7,8 @@ cardsCacheCapacity: 100 # Listen for incoming connections listen: false # Listen on a specific address, supports IPv4 and IPv6 -listenAddress: 0.0.0.0 +listenAddressIPv6: [::] +listenAddressIPv4: 0.0.0.0 # Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled! # - Use option "auto" to automatically detect support # - Use true or false (no qoutes) to enable or disable each protocol diff --git a/server.js b/server.js index ac146bc2d..61ead966d 100644 --- a/server.js +++ b/server.js @@ -131,7 +131,8 @@ if (process.versions && process.versions.node && process.versions.node.match(/20 const DEFAULT_PORT = 8000; const DEFAULT_AUTORUN = false; const DEFAULT_LISTEN = false; -const DEFAULT_LISTEN_ADDRESS = ''; +const DEFAULT_LISTEN_ADDRESS_IPV6 = '[::]'; +const DEFAULT_LISTEN_ADDRESS_IPV4 = '0.0.0.0'; const DEFAULT_CORS_PROXY = false; const DEFAULT_WHITELIST = true; const DEFAULT_ACCOUNTS = false; @@ -187,10 +188,14 @@ const cliArguments = yargs(hideBin(process.argv)) type: 'boolean', default: null, describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`, - }).option('listenAddress', { + }).option('listenAddressIPv6', { type: 'string', default: null, - describe: 'Set SillyTavern to listen to a specific address. If not set, it will fallback to listen to all.\n[config default: empty ]', + describe: 'Set SillyTavern to listen to a specific IPv6 address. If not set, it will fallback to listen to all.\n[config default: [::] ]', + }).option('listenAddressIPv4', { + type: 'string', + default: null, + describe: 'Set SillyTavern to listen to a specific IPv4 address. If not set, it will fallback to listen to all.\n[config default: 0.0.0.0 ]', }).option('corsProxy', { type: 'boolean', default: null, @@ -261,7 +266,9 @@ const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTOR /** @type {boolean} */ const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); /** @type {string} */ -const listenAddress = cliArguments.listenAddress ?? getConfigValue('listenAddress', DEFAULT_LISTEN_ADDRESS); +const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddressIPv6', DEFAULT_LISTEN_ADDRESS_IPV6); +/** @type {string} */ +const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddressIPv4', DEFAULT_LISTEN_ADDRESS_IPV4); /** @type {boolean} */ const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); @@ -716,13 +723,13 @@ app.use('/api/azure', azureRouter); const tavernUrlV6 = new URL( (cliArguments.ssl ? 'https://' : 'http://') + - (listen ? (ipRegex.v6({ exact: true }).test(listenAddress) ? `[${listenAddress}]` : '[::]') : '[::1]') + + (listen ? (ipRegex.v6({ exact: true }).test(listenAddressIPv6) ? listenAddressIPv6 : '[::]') : '[::1]') + (':' + server_port), ); const tavernUrl = new URL( (cliArguments.ssl ? 'https://' : 'http://') + - (listen ? (ipRegex.v4({ exact: true }).test(listenAddress) ? listenAddress : '0.0.0.0') : '127.0.0.1') + + (listen ? (ipRegex.v4({ exact: true }).test(listenAddressIPv4) ? listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') + (':' + server_port), ); @@ -789,10 +796,10 @@ const preSetupTasks = async function () { async function getAutorunHostname(useIPv6, useIPv4) { if (autorunHostname === 'auto') { if (listen) { - if (ipRegex.v4({ exact: true }).test(listenAddress)) { - return listenAddress; - } else if (ipRegex.v6({ exact: true }).test(listenAddress)) { - return `[${listenAddress}]`; + if (ipRegex.v6({ exact: true }).test(listenAddressIPv6)) { + return listenAddressIPv6; + } else if (ipRegex.v4({ exact: true }).test(listenAddressIPv4)) { + return listenAddressIPv4; } } @@ -858,13 +865,13 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); if (listen) { - if (ipRegex.v4({ exact: true }).test(listenAddress)) { + if (ipRegex.v6({ exact: true }).test(listenAddressIPv6)) { console.log( - `SillyTavern is listening on the address ${listenAddress}. 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`, + `SillyTavern is listening on the address ${listenAddressIPv6}. 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`, ); - } else if (ipRegex.v6({ exact: true }).test(listenAddress)) { + } else if (ipRegex.v4({ exact: true }).test(listenAddressIPv4)) { console.log( - `SillyTavern is listening on the address [${listenAddress}]. 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`, + `SillyTavern is listening on the address ${listenAddressIPv4}. 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`, ); } else { console.log( From a4c124dff06b3cb143d58d42c73139dd0156e0e8 Mon Sep 17 00:00:00 2001 From: Kristan Schlikow Date: Fri, 14 Feb 2025 20:00:15 +0100 Subject: [PATCH 10/22] Remove dead code --- server.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/server.js b/server.js index 61ead966d..61c940c09 100644 --- a/server.js +++ b/server.js @@ -941,19 +941,6 @@ function logSecurityAlert(message) { process.exit(1); } -/** - * Prints a warning message - * @param {string} message The warning message to print - * @returns {void} - */ -function logSecurityWarning(message) { - if (basicAuthMode || enableWhitelist) return; // safe! - console.error(color.yellow(message)); - if (getConfigValue('securityOverride', false)) { - console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.')); - } -} - /** * Handles the case where the server failed to start on one or both protocols. * @param {boolean} v6Failed If the server failed to start on IPv6 From f5bfbce0ad825ffad701a9b4d7c2b1b6c6f4ff97 Mon Sep 17 00:00:00 2001 From: Kristan Schlikow Date: Fri, 14 Feb 2025 20:04:23 +0100 Subject: [PATCH 11/22] Group listenAddress for config --- default/config.yaml | 5 +++-- server.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/default/config.yaml b/default/config.yaml index c84251b16..d6e31e07d 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -7,8 +7,9 @@ cardsCacheCapacity: 100 # Listen for incoming connections listen: false # Listen on a specific address, supports IPv4 and IPv6 -listenAddressIPv6: [::] -listenAddressIPv4: 0.0.0.0 +listenAddress: + ipv4: 0.0.0.0 + ipv6: [::] # Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled! # - Use option "auto" to automatically detect support # - Use true or false (no qoutes) to enable or disable each protocol diff --git a/server.js b/server.js index 61c940c09..d0eb09187 100644 --- a/server.js +++ b/server.js @@ -266,9 +266,9 @@ const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTOR /** @type {boolean} */ const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); /** @type {string} */ -const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddressIPv6', DEFAULT_LISTEN_ADDRESS_IPV6); +const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6); /** @type {string} */ -const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddressIPv4', DEFAULT_LISTEN_ADDRESS_IPV4); +const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4); /** @type {boolean} */ const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); From b029ae98dc06baca3c725e434a846d8f90f3b311 Mon Sep 17 00:00:00 2001 From: Kristan Schlikow Date: Fri, 14 Feb 2025 20:06:45 +0100 Subject: [PATCH 12/22] Fix default config for IPv6 --- default/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default/config.yaml b/default/config.yaml index d6e31e07d..4bd5da802 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -9,7 +9,7 @@ listen: false # Listen on a specific address, supports IPv4 and IPv6 listenAddress: ipv4: 0.0.0.0 - ipv6: [::] + ipv6: '[::]' # Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled! # - Use option "auto" to automatically detect support # - Use true or false (no qoutes) to enable or disable each protocol From 61e056004a768b7de90d708d488807a1beb2dca7 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 14 Feb 2025 22:06:18 +0200 Subject: [PATCH 13/22] Safari: hide summary block arrow --- public/style.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/style.css b/public/style.css index b56a87cd0..d045ab5c5 100644 --- a/public/style.css +++ b/public/style.css @@ -362,9 +362,14 @@ input[type='checkbox']:focus-visible { } .mes_reasoning_details .mes_reasoning_summary { + list-style: none; margin-right: calc(var(--mes-right-spacing) * -1); } +.mes_reasoning_details summary::-webkit-details-marker { + display: none; +} + .mes_reasoning *:last-child { margin-bottom: 0; } @@ -378,7 +383,7 @@ input[type='checkbox']:focus-visible { } .mes_reasoning_details .mes_reasoning em { - color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%) + color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%); } .mes_reasoning_header_block { @@ -403,7 +408,7 @@ input[type='checkbox']:focus-visible { } /* TWIMC: Remove with custom CSS to show the icon */ -.mes_reasoning_header > .icon-svg { +.mes_reasoning_header>.icon-svg { display: none; } From 13e38c7c8648695159fb31e8aa5bbba52ebffc09 Mon Sep 17 00:00:00 2001 From: Kristan Schlikow Date: Fri, 14 Feb 2025 21:13:17 +0100 Subject: [PATCH 14/22] Add debug script in package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 6f687a384..fe79108b5 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "version": "1.12.11", "scripts": { "start": "node server.js", + "debug": "node server.js --inspect", "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js", "start:bun": "bun server.js", "start:no-csrf": "node server.js --disableCsrf", From 961a71877b89dc338fae820f1f49436fa66cf96c Mon Sep 17 00:00:00 2001 From: Kristan Schlikow Date: Fri, 14 Feb 2025 22:02:16 +0100 Subject: [PATCH 15/22] Remove extra resolve for autorun --- server.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/server.js b/server.js index d0eb09187..b693f8d6d 100644 --- a/server.js +++ b/server.js @@ -795,14 +795,6 @@ const preSetupTasks = async function () { */ async function getAutorunHostname(useIPv6, useIPv4) { if (autorunHostname === 'auto') { - if (listen) { - if (ipRegex.v6({ exact: true }).test(listenAddressIPv6)) { - return listenAddressIPv6; - } else if (ipRegex.v4({ exact: true }).test(listenAddressIPv4)) { - return listenAddressIPv4; - } - } - let localhostResolve = await canResolve('localhost', useIPv6, useIPv4); if (useIPv6 && useIPv4) { From c98d241f3c93e6b94d4fc66e99739bffebf55738 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 14 Feb 2025 23:09:36 +0200 Subject: [PATCH 16/22] Refactor logAddress check --- server.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/server.js b/server.js index b693f8d6d..fdabe0b80 100644 --- a/server.js +++ b/server.js @@ -857,19 +857,16 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); if (listen) { - if (ipRegex.v6({ exact: true }).test(listenAddressIPv6)) { - console.log( - `SillyTavern is listening on the address ${listenAddressIPv6}. 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`, - ); - } else if (ipRegex.v4({ exact: true }).test(listenAddressIPv4)) { - console.log( - `SillyTavern is listening on the address ${listenAddressIPv4}. 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`, - ); - } else { - 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', - ); - } + const logAddress = ipRegex.v6({ exact: true }).test(listenAddressIPv6) + ? listenAddressIPv6 + : ipRegex.v4({ exact: true }).test(listenAddressIPv4) + ? listenAddressIPv4 + : null; + console.log( + logAddress + ? `SillyTavern is listening on the address ${logAddress}. 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` + : '[::] 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', + ); } if (basicAuthMode) { From d276d233105822a9aa2ca41f45a127fce360e2f7 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 14 Feb 2025 23:20:33 +0200 Subject: [PATCH 17/22] Firefox: Clear cache on avatar upload --- src/endpoints/characters.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 8c39575ed..294a195dc 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -839,6 +839,9 @@ router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async functi invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url); await writeCharacterData(newAvatarPath, char, targetFile, request, crop); fs.unlinkSync(newAvatarPath); + + // Bust cache to reload the new avatar + response.setHeader('Clear-Site-Data', '"cache"'); } return response.sendStatus(200); From dbbf069e85efc8c3e5df005f06aeb8c046c12c64 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 14 Feb 2025 23:53:31 +0200 Subject: [PATCH 18/22] Remove message redundancy --- server.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/server.js b/server.js index fdabe0b80..6f5634f41 100644 --- a/server.js +++ b/server.js @@ -852,22 +852,15 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { const plainGoToLog = removeColorFormatting(goToLog); console.log(logListen); + if (listen) { + console.log(); + console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".'); + console.log('Check the "access.log" file in the SillyTavern directory to inspect incoming connections.'); + } console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); console.log(goToLog); console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); - if (listen) { - const logAddress = ipRegex.v6({ exact: true }).test(listenAddressIPv6) - ? listenAddressIPv6 - : ipRegex.v4({ exact: true }).test(listenAddressIPv4) - ? listenAddressIPv4 - : null; - console.log( - logAddress - ? `SillyTavern is listening on the address ${logAddress}. 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` - : '[::] 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', - ); - } if (basicAuthMode) { if (perUserBasicAuth && !enableAccounts) { From dd50f491760d7109bf687b48403f526f14a480fa Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 15 Feb 2025 10:23:36 +0100 Subject: [PATCH 19/22] Fix auto expand on thinking during new message --- public/scripts/reasoning.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js index 76f1f0852..d2445316e 100644 --- a/public/scripts/reasoning.js +++ b/public/scripts/reasoning.js @@ -215,6 +215,10 @@ export class ReasoningHandler { } this.updateDom(messageId); + + if (power_user.reasoning.auto_expand) { + this.messageReasoningDetailsDom.open = true; + } } /** From bae02a44ed6f387df1c60f9ed4bfc064401ccd62 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 15 Feb 2025 12:56:43 +0200 Subject: [PATCH 20/22] Add cache buster middleware to clear browser cache on server restart --- server.js | 3 ++- src/middleware/cacheBuster.js | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/middleware/cacheBuster.js diff --git a/server.js b/server.js index 6f5634f41..18f5cba1a 100644 --- a/server.js +++ b/server.js @@ -60,6 +60,7 @@ import basicAuthMiddleware from './src/middleware/basicAuth.js'; import whitelistMiddleware from './src/middleware/whitelist.js'; import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; import initRequestProxy from './src/request-proxy.js'; +import getCacheBusterMiddleware from './src/middleware/cacheBuster.js'; import { getVersion, getConfigValue, @@ -515,7 +516,7 @@ if (!disableCsrf) { // Static files // Host index page -app.get('/', (request, response) => { +app.get('/', getCacheBusterMiddleware(), (request, response) => { if (shouldRedirectToLogin(request)) { const query = request.url.split('?')[1]; const redirectUrl = query ? `/login?${query}` : '/login'; diff --git a/src/middleware/cacheBuster.js b/src/middleware/cacheBuster.js new file mode 100644 index 000000000..c197edc6a --- /dev/null +++ b/src/middleware/cacheBuster.js @@ -0,0 +1,22 @@ +/** + * Middleware to bust the browser cache for the current user. + * @returns {import('express').RequestHandler} + */ +export default function getCacheBusterMiddleware() { + /** + * @type {Set} Handles that have already been busted. + */ + const handles = new Set(); + + return (request, response, next) => { + const handle = request.user?.profile?.handle; + + if (!handle || handles.has(handle)) { + return next(); + } + + handles.add(handle); + response.setHeader('Clear-Site-Data', '"cache"'); + next(); + }; +} From 37d9d4c25340367af18caaa0cc1f6dc3fbdd52bb Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 15 Feb 2025 21:44:06 +0100 Subject: [PATCH 21/22] Make sure auto-expand doesn't conflict with hidden --- public/scripts/reasoning.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js index d2445316e..83154437d 100644 --- a/public/scripts/reasoning.js +++ b/public/scripts/reasoning.js @@ -216,7 +216,7 @@ export class ReasoningHandler { this.updateDom(messageId); - if (power_user.reasoning.auto_expand) { + if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) { this.messageReasoningDetailsDom.open = true; } } From fd38ca503a277eb753475b1827ada6264dcb0a6d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 15 Feb 2025 23:05:08 +0200 Subject: [PATCH 22/22] Evict cache per user agent --- src/middleware/cacheBuster.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/middleware/cacheBuster.js b/src/middleware/cacheBuster.js index c197edc6a..5c38d2da1 100644 --- a/src/middleware/cacheBuster.js +++ b/src/middleware/cacheBuster.js @@ -1,21 +1,27 @@ +import crypto from 'node:crypto'; +import { DEFAULT_USER } from '../constants.js'; + /** * Middleware to bust the browser cache for the current user. * @returns {import('express').RequestHandler} */ export default function getCacheBusterMiddleware() { /** - * @type {Set} Handles that have already been busted. + * @type {Set} Handles/User-Agents that have already been busted. */ - const handles = new Set(); + const keys = new Set(); return (request, response, next) => { - const handle = request.user?.profile?.handle; + const handle = request.user?.profile?.handle || DEFAULT_USER.handle; + const userAgent = request.headers['user-agent'] || ''; + const hash = crypto.createHash('sha256').update(userAgent).digest('hex'); + const key = `${handle}-${hash}`; - if (!handle || handles.has(handle)) { + if (keys.has(key)) { return next(); } - handles.add(handle); + keys.add(key); response.setHeader('Clear-Site-Data', '"cache"'); next(); };