Merge branch 'staging' into persona-improvements
| @@ -45,6 +45,7 @@ access.log | |||
| 45 | /vectors/ | 45 | /vectors/ |
| 46 | /cache/ | 46 | /cache/ |
| 47 | public/css/user.css | 47 | public/css/user.css |
| 48 | public/error/ | ||
| 48 | /plugins/ | 49 | /plugins/ |
| 49 | /data | 50 | /data |
| 50 | /default/scaffold | 51 | /default/scaffold |
| @@ -52,3 +53,5 @@ public/scripts/extensions/third-party | |||
| 52 | /certs | 53 | /certs |
| 53 | .aider* | 54 | .aider* |
| 54 | .env | 55 | .env |
| 56 | /StartDev.bat | ||
| 57 | |||
| @@ -70,7 +70,7 @@ perUserBasicAuth: false | |||
| 70 | ## Set to a positive number to expire session after a certain time of inactivity | 70 | ## Set to a positive number to expire session after a certain time of inactivity |
| 71 | ## Set to 0 to expire session when the browser is closed | 71 | ## Set to 0 to expire session when the browser is closed |
| 72 | ## Set to a negative number to disable session expiration | 72 | ## Set to a negative number to disable session expiration |
| 73 | sessionTimeout: 86400 | 73 | sessionTimeout: -1 |
| 74 | # Used to sign session cookies. Will be auto-generated if not set | 74 | # Used to sign session cookies. Will be auto-generated if not set |
| 75 | cookieSecret: '' | 75 | cookieSecret: '' |
| 76 | # Disable CSRF protection - NOT RECOMMENDED | 76 | # Disable CSRF protection - NOT RECOMMENDED |
| @@ -0,0 +1,22 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html> | ||
| 3 | |||
| 4 | <head> | ||
| 5 | <title>Forbidden</title> | ||
| 6 | </head> | ||
| 7 | |||
| 8 | <body> | ||
| 9 | <h1>Forbidden</h1> | ||
| 10 | <p> | ||
| 11 | If you are the system administrator, add your IP address to the | ||
| 12 | whitelist or disable whitelist mode by editing | ||
| 13 | <code>config.yaml</code> in the root directory of your installation. | ||
| 14 | </p> | ||
| 15 | <hr /> | ||
| 16 | <p> | ||
| 17 | <em>Connection from {{ipDetails}} has been blocked. This attempt | ||
| 18 | has been logged.</em> | ||
| 19 | </p> | ||
| 20 | </body> | ||
| 21 | |||
| 22 | </html> | ||
| @@ -0,0 +1,17 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html> | ||
| 3 | |||
| 4 | <head> | ||
| 5 | <title>Unauthorized</title> | ||
| 6 | </head> | ||
| 7 | |||
| 8 | <body> | ||
| 9 | <h1>Unauthorized</h1> | ||
| 10 | <p> | ||
| 11 | If you are the system administrator, you can configure the | ||
| 12 | <code>basicAuthUser</code> credentials by editing | ||
| 13 | <code>config.yaml</code> in the root directory of your installation. | ||
| 14 | </p> | ||
| 15 | </body> | ||
| 16 | |||
| 17 | </html> | ||
| @@ -0,0 +1,15 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html> | ||
| 3 | |||
| 4 | <head> | ||
| 5 | <title>Not found</title> | ||
| 6 | </head> | ||
| 7 | |||
| 8 | <body> | ||
| 9 | <h1>Not found</h1> | ||
| 10 | <p> | ||
| 11 | The requested URL was not found on this server. | ||
| 12 | </p> | ||
| 13 | </body> | ||
| 14 | |||
| 15 | </html> | ||
| @@ -1,6 +1,24 @@ | |||
| 1 | import { UserDirectoryList, User } from "./src/users"; | 1 | import { UserDirectoryList, User } from "./src/users"; |
| 2 | import { CsrfSyncedToken } from "csrf-sync"; | ||
| 2 | 3 | ||
| 3 | declare global { | 4 | declare global { |
| 5 | declare namespace CookieSessionInterfaces { | ||
| 6 | export interface CookieSessionObject { | ||
| 7 | /** | ||
| 8 | * The CSRF token for the session. | ||
| 9 | */ | ||
| 10 | csrfToken: CsrfSyncedToken; | ||
| 11 | /** | ||
| 12 | * Authenticated user handle. | ||
| 13 | */ | ||
| 14 | handle: string; | ||
| 15 | /** | ||
| 16 | * Last time the session was extended. | ||
| 17 | */ | ||
| 18 | touch: number; | ||
| 19 | } | ||
| 20 | } | ||
| 21 | |||
| 4 | namespace Express { | 22 | namespace Express { |
| 5 | export interface Request { | 23 | export interface Request { |
| 6 | user: { | 24 | user: { |
| @@ -15,11 +33,3 @@ declare global { | |||
| 15 | */ | 33 | */ |
| 16 | var DATA_ROOT: string; | 34 | var DATA_ROOT: string; |
| 17 | } | 35 | } |
| 18 | |||
| 19 | declare module 'express-session' { | ||
| 20 | export interface SessionData { | ||
| 21 | handle: string; | ||
| 22 | touch: number; | ||
| 23 | // other properties... | ||
| 24 | } | ||
| 25 | } | ||
| @@ -26,7 +26,7 @@ | |||
| 26 | "cookie-parser": "^1.4.6", | 26 | "cookie-parser": "^1.4.6", |
| 27 | "cookie-session": "^2.1.0", | 27 | "cookie-session": "^2.1.0", |
| 28 | "cors": "^2.8.5", | 28 | "cors": "^2.8.5", |
| 29 | "csrf-csrf": "^2.2.3", | 29 | "csrf-sync": "^4.0.3", |
| 30 | "diff-match-patch": "^1.0.5", | 30 | "diff-match-patch": "^1.0.5", |
| 31 | "dompurify": "^3.1.7", | 31 | "dompurify": "^3.1.7", |
| 32 | "droll": "^0.2.1", | 32 | "droll": "^0.2.1", |
| @@ -2987,10 +2987,10 @@ | |||
| 2987 | "node": "*" | 2987 | "node": "*" |
| 2988 | } | 2988 | } |
| 2989 | }, | 2989 | }, |
| 2990 | "node_modules/csrf-csrf": { | 2990 | "node_modules/csrf-sync": { |
| 2991 | "version": "2.2.4", | 2991 | "version": "4.0.3", |
| 2992 | "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-2.2.4.tgz", | 2992 | "resolved": "https://registry.npmjs.org/csrf-sync/-/csrf-sync-4.0.3.tgz", |
| 2993 | "integrity": "sha512-LuhBmy5RfRmEfeqeYqgaAuS1eDpVtKZB/Eiec9xiKQLBynJxrGVRdM2yRT/YMl1Njo/yKh2L9AYsIwSlTPnx2A==", | 2993 | "integrity": "sha512-wXzltBBzt/7imzDt6ZT7G/axQG7jo4Sm0uXDUzFY8hR59qhDHdjqpW2hojS4oAVIZDzwlMQloIVCTJoDDh0wwA==", |
| 2994 | "license": "ISC", | 2994 | "license": "ISC", |
| 2995 | "dependencies": { | 2995 | "dependencies": { |
| 2996 | "http-errors": "^2.0.0" | 2996 | "http-errors": "^2.0.0" |
| @@ -16,7 +16,7 @@ | |||
| 16 | "cookie-parser": "^1.4.6", | 16 | "cookie-parser": "^1.4.6", |
| 17 | "cookie-session": "^2.1.0", | 17 | "cookie-session": "^2.1.0", |
| 18 | "cors": "^2.8.5", | 18 | "cors": "^2.8.5", |
| 19 | "csrf-csrf": "^2.2.3", | 19 | "csrf-sync": "^4.0.3", |
| 20 | "diff-match-patch": "^1.0.5", | 20 | "diff-match-patch": "^1.0.5", |
| 21 | "dompurify": "^3.1.7", | 21 | "dompurify": "^3.1.7", |
| 22 | "droll": "^0.2.1", | 22 | "droll": "^0.2.1", |
| @@ -213,20 +213,60 @@ function addMissingConfigValues() { | |||
| 213 | * Creates the default config files if they don't exist yet. | 213 | * Creates the default config files if they don't exist yet. |
| 214 | */ | 214 | */ |
| 215 | function createDefaultFiles() { | 215 | function createDefaultFiles() { |
| 216 | const files = { | 216 | /** |
| 217 | config: './config.yaml', | 217 | * @typedef DefaultItem |
| 218 | user: './public/css/user.css', | 218 | * @type {object} |
| 219 | }; | 219 | * @property {'file' | 'directory'} type - Whether the item should be copied as a single file or merged into a directory structure. |
| 220 | * @property {string} defaultPath - The path to the default item (typically in `default/`). | ||
| 221 | * @property {string} productionPath - The path to the copied item for production use. | ||
| 222 | */ | ||
| 223 | |||
| 224 | /** @type {DefaultItem[]} */ | ||
| 225 | const defaultItems = [ | ||
| 226 | { | ||
| 227 | type: 'file', | ||
| 228 | defaultPath: './default/config.yaml', | ||
| 229 | productionPath: './config.yaml', | ||
| 230 | }, | ||
| 231 | { | ||
| 232 | type: 'directory', | ||
| 233 | defaultPath: './default/public/', | ||
| 234 | productionPath: './public/', | ||
| 235 | }, | ||
| 236 | ]; | ||
| 220 | 237 | ||
| 221 | for (const file of Object.values(files)) { | 238 | for (const defaultItem of defaultItems) { |
| 222 | try { | 239 | try { |
| 223 | if (!fs.existsSync(file)) { | 240 | if (defaultItem.type === 'file') { |
| 224 | const defaultFilePath = path.join('./default', path.parse(file).base); | 241 | if (!fs.existsSync(defaultItem.productionPath)) { |
| 225 | fs.copyFileSync(defaultFilePath, file); | 242 | fs.copyFileSync( |
| 226 | console.log(color.green(`Created default file: ${file}`)); | 243 | defaultItem.defaultPath, |
| 244 | defaultItem.productionPath, | ||
| 245 | ); | ||
| 246 | console.log( | ||
| 247 | color.green(`Created default file: ${defaultItem.productionPath}`), | ||
| 248 | ); | ||
| 249 | } | ||
| 250 | } else if (defaultItem.type === 'directory') { | ||
| 251 | fs.cpSync(defaultItem.defaultPath, defaultItem.productionPath, { | ||
| 252 | force: false, // Don't overwrite existing files! | ||
| 253 | recursive: true, | ||
| 254 | }); | ||
| 255 | console.log( | ||
| 256 | color.green(`Synchronized missing files: ${defaultItem.productionPath}`), | ||
| 257 | ); | ||
| 258 | } else { | ||
| 259 | throw new Error( | ||
| 260 | 'FATAL: Unexpected default file format in `post-install.js#createDefaultFiles()`.', | ||
| 261 | ); | ||
| 227 | } | 262 | } |
| 228 | } catch (error) { | 263 | } catch (error) { |
| 229 | console.error(color.red(`FATAL: Could not write default file: ${file}`), error); | 264 | console.error( |
| 265 | color.red( | ||
| 266 | `FATAL: Could not write default ${defaultItem.type}: ${defaultItem.productionPath}`, | ||
| 267 | ), | ||
| 268 | error, | ||
| 269 | ); | ||
| 230 | } | 270 | } |
| 231 | } | 271 | } |
| 232 | } | 272 | } |
| @@ -6372,7 +6372,10 @@ | |||
| 6372 | <img alt="Avatar" src="" /> | 6372 | <img alt="Avatar" src="" /> |
| 6373 | </div> | 6373 | </div> |
| 6374 | <div class="group_member_name"> | 6374 | <div class="group_member_name"> |
| 6375 | <div class="ch_name"></div> | 6375 | <div class="character_name_block"> |
| 6376 | <span class="ch_name"></span> | ||
| 6377 | <small class="ch_additional_info character_version"></small> | ||
| 6378 | </div> | ||
| 6376 | <div class="tags tags_inline"></div> | 6379 | <div class="tags tags_inline"></div> |
| 6377 | </div> | 6380 | </div> |
| 6378 | <input class="ch_fav" value="" hidden /> | 6381 | <input class="ch_fav" value="" hidden /> |
| @@ -1376,7 +1376,7 @@ | |||
| 1376 | "char_import_2": "Chub Lorebook (رابط مباشر أو معرف)", | 1376 | "char_import_2": "Chub Lorebook (رابط مباشر أو معرف)", |
| 1377 | "char_import_3": "حرف JanitorAI (رابط مباشر أو UUID)", | 1377 | "char_import_3": "حرف JanitorAI (رابط مباشر أو UUID)", |
| 1378 | "char_import_4": "حرف Pygmalion.chat (رابط مباشر أو UUID)", | 1378 | "char_import_4": "حرف Pygmalion.chat (رابط مباشر أو UUID)", |
| 1379 | "char_import_5": "حرف AICharacterCard.com (رابط مباشر أو معرف)", | 1379 | "char_import_5": "حرف AICharacterCards.com (رابط مباشر أو معرف)", |
| 1380 | "char_import_6": "رابط PNG المباشر (راجع", | 1380 | "char_import_6": "رابط PNG المباشر (راجع", |
| 1381 | "char_import_7": "للمضيفين المسموح بهم)", | 1381 | "char_import_7": "للمضيفين المسموح بهم)", |
| 1382 | "char_import_8": "شخصية RisuRealm (رابط مباشر)", | 1382 | "char_import_8": "شخصية RisuRealm (رابط مباشر)", |
| @@ -1376,7 +1376,7 @@ | |||
| 1376 | "char_import_2": "Chub Lorebook (Direktlink oder ID)", | 1376 | "char_import_2": "Chub Lorebook (Direktlink oder ID)", |
| 1377 | "char_import_3": "JanitorAI-Charakter (Direktlink oder UUID)", | 1377 | "char_import_3": "JanitorAI-Charakter (Direktlink oder UUID)", |
| 1378 | "char_import_4": "Pygmalion.chat-Charakter (Direktlink oder UUID)", | 1378 | "char_import_4": "Pygmalion.chat-Charakter (Direktlink oder UUID)", |
| 1379 | "char_import_5": "AICharacterCard.com-Charakter (Direktlink oder ID)", | 1379 | "char_import_5": "AICharacterCards.com-Charakter (Direktlink oder ID)", |
| 1380 | "char_import_6": "Direkter PNG-Link (siehe", | 1380 | "char_import_6": "Direkter PNG-Link (siehe", |
| 1381 | "char_import_7": "für erlaubte Hosts)", | 1381 | "char_import_7": "für erlaubte Hosts)", |
| 1382 | "char_import_8": "RisuRealm-Charakter (Direktlink)", | 1382 | "char_import_8": "RisuRealm-Charakter (Direktlink)", |
| @@ -1376,7 +1376,7 @@ | |||
| 1376 | "char_import_2": "Chub Lorebook (enlace directo o ID)", | 1376 | "char_import_2": "Chub Lorebook (enlace directo o ID)", |
| 1377 | "char_import_3": "Carácter de JanitorAI (enlace directo o UUID)", | 1377 | "char_import_3": "Carácter de JanitorAI (enlace directo o UUID)", |
| 1378 | "char_import_4": "Carácter Pygmalion.chat (enlace directo o UUID)", | 1378 | "char_import_4": "Carácter Pygmalion.chat (enlace directo o UUID)", |
| 1379 | "char_import_5": "Carácter AICharacterCard.com (enlace directo o ID)", | 1379 | "char_import_5": "Carácter AICharacterCards.com (enlace directo o ID)", |
| 1380 | "char_import_6": "Enlace PNG directo (consulte", | 1380 | "char_import_6": "Enlace PNG directo (consulte", |
| 1381 | "char_import_7": "para hosts permitidos)", | 1381 | "char_import_7": "para hosts permitidos)", |
| 1382 | "char_import_8": "Personaje RisuRealm (Enlace directo)", | 1382 | "char_import_8": "Personaje RisuRealm (Enlace directo)", |
| @@ -1297,7 +1297,7 @@ | |||
| 1297 | "char_import_2": "Lorebook de Chub (lien direct ou ID)", | 1297 | "char_import_2": "Lorebook de Chub (lien direct ou ID)", |
| 1298 | "char_import_3": "Personnage de JanitorAI (lien direct ou UUID)", | 1298 | "char_import_3": "Personnage de JanitorAI (lien direct ou UUID)", |
| 1299 | "char_import_4": "Personnage de Pygmalion.chat (lien direct ou UUID)", | 1299 | "char_import_4": "Personnage de Pygmalion.chat (lien direct ou UUID)", |
| 1300 | "char_import_5": "Personnage de AICharacterCard.com (lien direct ou identifiant)", | 1300 | "char_import_5": "Personnage de AICharacterCards.com (lien direct ou identifiant)", |
| 1301 | "char_import_6": "Lien PNG direct (voir", | 1301 | "char_import_6": "Lien PNG direct (voir", |
| 1302 | "char_import_7": "pour les hôtes autorisés)", | 1302 | "char_import_7": "pour les hôtes autorisés)", |
| 1303 | "char_import_8": "Personnage de RisuRealm (lien direct)", | 1303 | "char_import_8": "Personnage de RisuRealm (lien direct)", |
| @@ -1376,7 +1376,7 @@ | |||
| 1376 | "char_import_2": "Chub Lorebook (beinn hlekkur eða auðkenni)", | 1376 | "char_import_2": "Chub Lorebook (beinn hlekkur eða auðkenni)", |
| 1377 | "char_import_3": "JanitorAI karakter (beinn hlekkur eða UUID)", | 1377 | "char_import_3": "JanitorAI karakter (beinn hlekkur eða UUID)", |
| 1378 | "char_import_4": "Pygmalion.chat karakter (beinn hlekkur eða UUID)", | 1378 | "char_import_4": "Pygmalion.chat karakter (beinn hlekkur eða UUID)", |
| 1379 | "char_import_5": "AICharacterCard.com Karakter (beinn hlekkur eða auðkenni)", | 1379 | "char_import_5": "AICharacterCards.com Karakter (beinn hlekkur eða auðkenni)", |
| 1380 | "char_import_6": "Beinn PNG hlekkur (sjá", | 1380 | "char_import_6": "Beinn PNG hlekkur (sjá", |
| 1381 | "char_import_7": "fyrir leyfilega gestgjafa)", | 1381 | "char_import_7": "fyrir leyfilega gestgjafa)", |
| 1382 | "char_import_8": "RisuRealm karakter (beinn hlekkur)", | 1382 | "char_import_8": "RisuRealm karakter (beinn hlekkur)", |
| @@ -1376,7 +1376,7 @@ | |||
| 1376 | "char_import_2": "Lorebook di Chub (collegamento diretto o ID)", | 1376 | "char_import_2": "Lorebook di Chub (collegamento diretto o ID)", |
| 1377 | "char_import_3": "Carattere JanitorAI (collegamento diretto o UUID)", | 1377 | "char_import_3": "Carattere JanitorAI (collegamento diretto o UUID)", |
| 1378 | "char_import_4": "Carattere Pygmalion.chat (collegamento diretto o UUID)", | 1378 | "char_import_4": "Carattere Pygmalion.chat (collegamento diretto o UUID)", |
| 1379 | "char_import_5": "Carattere AICharacterCard.com (Link diretto o ID)", | 1379 | "char_import_5": "Carattere AICharacterCards.com (Link diretto o ID)", |
| 1380 | "char_import_6": "Collegamento PNG diretto (fare riferimento a", | 1380 | "char_import_6": "Collegamento PNG diretto (fare riferimento a", |
| 1381 | "char_import_7": "per gli host consentiti)", | 1381 | "char_import_7": "per gli host consentiti)", |
| 1382 | "char_import_8": "Personaggio RisuRealm (collegamento diretto)", | 1382 | "char_import_8": "Personaggio RisuRealm (collegamento diretto)", |
| @@ -1378,7 +1378,7 @@ | |||
| 1378 | "char_import_2": "Chub ロアブック (直接リンクまたは ID)", | 1378 | "char_import_2": "Chub ロアブック (直接リンクまたは ID)", |
| 1379 | "char_import_3": "JanitorAI キャラクター (直接リンクまたは UUID)", | 1379 | "char_import_3": "JanitorAI キャラクター (直接リンクまたは UUID)", |
| 1380 | "char_import_4": "Pygmalion.chat キャラクター (直接リンクまたは UUID)", | 1380 | "char_import_4": "Pygmalion.chat キャラクター (直接リンクまたは UUID)", |
| 1381 | "char_import_5": "AICharacterCard.com キャラクター (直接リンクまたは ID)", | 1381 | "char_import_5": "AICharacterCards.com キャラクター (直接リンクまたは ID)", |
| 1382 | "char_import_6": "直接PNGリンク(参照", | 1382 | "char_import_6": "直接PNGリンク(参照", |
| 1383 | "char_import_7": "許可されたホストの場合)", | 1383 | "char_import_7": "許可されたホストの場合)", |
| 1384 | "char_import_8": "RisuRealm キャラクター (直接リンク)", | 1384 | "char_import_8": "RisuRealm キャラクター (直接リンク)", |
| @@ -1395,7 +1395,7 @@ | |||
| 1395 | "char_import_2": "Chub Lorebook(직접 링크 또는 ID)", | 1395 | "char_import_2": "Chub Lorebook(직접 링크 또는 ID)", |
| 1396 | "char_import_3": "JanitorAI 캐릭터(직접 링크 또는 UUID)", | 1396 | "char_import_3": "JanitorAI 캐릭터(직접 링크 또는 UUID)", |
| 1397 | "char_import_4": "Pygmalion.chat 문자(직접 링크 또는 UUID)", | 1397 | "char_import_4": "Pygmalion.chat 문자(직접 링크 또는 UUID)", |
| 1398 | "char_import_5": "AICharacterCard.com 캐릭터(직접 링크 또는 ID)", | 1398 | "char_import_5": "AICharacterCards.com 캐릭터(직접 링크 또는 ID)", |
| 1399 | "char_import_6": "직접 PNG 링크(참조", | 1399 | "char_import_6": "직접 PNG 링크(참조", |
| 1400 | "char_import_7": "허용된 호스트의 경우)", | 1400 | "char_import_7": "허용된 호스트의 경우)", |
| 1401 | "char_import_8": "RisuRealm 캐릭터 (직접링크)", | 1401 | "char_import_8": "RisuRealm 캐릭터 (직접링크)", |
| @@ -1376,7 +1376,7 @@ | |||
| 1376 | "char_import_2": "Chub Lorebook (directe link of ID)", | 1376 | "char_import_2": "Chub Lorebook (directe link of ID)", |
| 1377 | "char_import_3": "JanitorAI-personage (directe link of UUID)", | 1377 | "char_import_3": "JanitorAI-personage (directe link of UUID)", |
| 1378 | "char_import_4": "Pygmalion.chat-teken (directe link of UUID)", | 1378 | "char_import_4": "Pygmalion.chat-teken (directe link of UUID)", |
| 1379 | "char_import_5": "AICharacterCard.com-teken (directe link of ID)", | 1379 | "char_import_5": "AICharacterCards.com-teken (directe link of ID)", |
| 1380 | "char_import_6": "Directe PNG-link (zie", | 1380 | "char_import_6": "Directe PNG-link (zie", |
| 1381 | "char_import_7": "voor toegestane hosts)", | 1381 | "char_import_7": "voor toegestane hosts)", |
| 1382 | "char_import_8": "RisuRealm-personage (directe link)", | 1382 | "char_import_8": "RisuRealm-personage (directe link)", |
| @@ -1376,7 +1376,7 @@ | |||
| 1376 | "char_import_2": "Chub Lorebook (link direto ou ID)", | 1376 | "char_import_2": "Chub Lorebook (link direto ou ID)", |
| 1377 | "char_import_3": "Personagem JanitorAI (Link Direto ou UUID)", | 1377 | "char_import_3": "Personagem JanitorAI (Link Direto ou UUID)", |
| 1378 | "char_import_4": "Caractere Pygmalion.chat (Link Direto ou UUID)", | 1378 | "char_import_4": "Caractere Pygmalion.chat (Link Direto ou UUID)", |
| 1379 | "char_import_5": "Personagem AICharacterCard.com (link direto ou ID)", | 1379 | "char_import_5": "Personagem AICharacterCards.com (link direto ou ID)", |
| 1380 | "char_import_6": "Link PNG direto (consulte", | 1380 | "char_import_6": "Link PNG direto (consulte", |
| 1381 | "char_import_7": "para hosts permitidos)", | 1381 | "char_import_7": "para hosts permitidos)", |
| 1382 | "char_import_8": "Personagem RisuRealm (link direto)", | 1382 | "char_import_8": "Personagem RisuRealm (link direto)", |
| @@ -966,7 +966,7 @@ | |||
| 966 | "char_import_2": "Лорбук с Chub (прямая ссылка или ID)", | 966 | "char_import_2": "Лорбук с Chub (прямая ссылка или ID)", |
| 967 | "char_import_3": "Персонаж с JanitorAI (прямая ссылка или UUID)", | 967 | "char_import_3": "Персонаж с JanitorAI (прямая ссылка или UUID)", |
| 968 | "char_import_4": "Персонаж с Pygmalion.chat (прямая ссылка или UUID)", | 968 | "char_import_4": "Персонаж с Pygmalion.chat (прямая ссылка или UUID)", |
| 969 | "char_import_5": "Персонаж с AICharacterCard.com (прямая ссылка или ID)", | 969 | "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)", |
| 970 | "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в", | 970 | "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в", |
| 971 | "char_import_7": ")", | 971 | "char_import_7": ")", |
| 972 | "Grammar String": "Грамматика", | 972 | "Grammar String": "Грамматика", |
| @@ -1376,7 +1376,7 @@ | |||
| 1376 | "char_import_2": "Chub Lorebook (пряме посилання або ID)", | 1376 | "char_import_2": "Chub Lorebook (пряме посилання або ID)", |
| 1377 | "char_import_3": "Символ JanitorAI (пряме посилання або UUID)", | 1377 | "char_import_3": "Символ JanitorAI (пряме посилання або UUID)", |
| 1378 | "char_import_4": "Символ Pygmalion.chat (пряме посилання або UUID)", | 1378 | "char_import_4": "Символ Pygmalion.chat (пряме посилання або UUID)", |
| 1379 | "char_import_5": "Символ AICharacterCard.com (пряме посилання або ідентифікатор)", | 1379 | "char_import_5": "Символ AICharacterCards.com (пряме посилання або ідентифікатор)", |
| 1380 | "char_import_6": "Пряме посилання на PNG (див", | 1380 | "char_import_6": "Пряме посилання на PNG (див", |
| 1381 | "char_import_7": "для дозволених хостів)", | 1381 | "char_import_7": "для дозволених хостів)", |
| 1382 | "char_import_8": "Персонаж RisuRealm (пряме посилання)", | 1382 | "char_import_8": "Персонаж RisuRealm (пряме посилання)", |
| @@ -1376,7 +1376,7 @@ | |||
| 1376 | "char_import_2": "Chub (Nhập URL trực tiếp hoặc ID)", | 1376 | "char_import_2": "Chub (Nhập URL trực tiếp hoặc ID)", |
| 1377 | "char_import_3": "JanitorAI (Nhập URL trực tiếp hoặc UUID)", | 1377 | "char_import_3": "JanitorAI (Nhập URL trực tiếp hoặc UUID)", |
| 1378 | "char_import_4": "Pygmalion.chat (Nhập URL trực tiếp hoặc UUID)", | 1378 | "char_import_4": "Pygmalion.chat (Nhập URL trực tiếp hoặc UUID)", |
| 1379 | "char_import_5": "AICharacterCard.com (Nhập URL trực tiếp hoặc ID)", | 1379 | "char_import_5": "AICharacterCards.com (Nhập URL trực tiếp hoặc ID)", |
| 1380 | "char_import_6": "Nhập PNG trực tiếp (tham khảo", | 1380 | "char_import_6": "Nhập PNG trực tiếp (tham khảo", |
| 1381 | "char_import_7": "đối với các máy chủ được phép)", | 1381 | "char_import_7": "đối với các máy chủ được phép)", |
| 1382 | "char_import_8": "RisuRealm (URL trực tiếp)", | 1382 | "char_import_8": "RisuRealm (URL trực tiếp)", |
| @@ -1829,7 +1829,7 @@ | |||
| 1829 | "char_import_2": "Chub 知识书(直链或ID)", | 1829 | "char_import_2": "Chub 知识书(直链或ID)", |
| 1830 | "char_import_3": "JanitorAI 角色(直链或UUID)", | 1830 | "char_import_3": "JanitorAI 角色(直链或UUID)", |
| 1831 | "char_import_4": "Pygmalion.chat 角色(直链或UUID)", | 1831 | "char_import_4": "Pygmalion.chat 角色(直链或UUID)", |
| 1832 | "char_import_5": "AICharacterCard.com 角色(直链或ID)", | 1832 | "char_import_5": "AICharacterCards.com 角色(直链或ID)", |
| 1833 | "char_import_6": "被允许的PNG直链(请参阅", | 1833 | "char_import_6": "被允许的PNG直链(请参阅", |
| 1834 | "char_import_7": ")", | 1834 | "char_import_7": ")", |
| 1835 | "char_import_8": "RisuRealm 角色(直链)", | 1835 | "char_import_8": "RisuRealm 角色(直链)", |
| @@ -1381,7 +1381,7 @@ | |||
| 1381 | "char_import_2": "Chub Lorebook(直接連結或 ID)", | 1381 | "char_import_2": "Chub Lorebook(直接連結或 ID)", |
| 1382 | "char_import_3": "JanitorAI 角色(直接連結或 ID)", | 1382 | "char_import_3": "JanitorAI 角色(直接連結或 ID)", |
| 1383 | "char_import_4": "Pygmalion.chat 角色(直接連結或 ID)", | 1383 | "char_import_4": "Pygmalion.chat 角色(直接連結或 ID)", |
| 1384 | "char_import_5": "AICharacterCard.com 角色(直接連結或 ID)", | 1384 | "char_import_5": "AICharacterCards.com 角色(直接連結或 ID)", |
| 1385 | "char_import_6": "直接 PNG 連結(請參閱", | 1385 | "char_import_6": "直接 PNG 連結(請參閱", |
| 1386 | "char_import_7": "對於允許的主機)", | 1386 | "char_import_7": "對於允許的主機)", |
| 1387 | "char_import_8": "RisuRealm角色(直接連結)", | 1387 | "char_import_8": "RisuRealm角色(直接連結)", |
| @@ -1368,6 +1368,15 @@ function getGroupCharacterBlock(character) { | |||
| 1368 | template.find('.ch_fav').val(isFav); | 1368 | template.find('.ch_fav').val(isFav); |
| 1369 | template.toggleClass('is_fav', isFav); | 1369 | template.toggleClass('is_fav', isFav); |
| 1370 | 1370 | ||
| 1371 | const auxFieldName = power_user.aux_field || 'character_version'; | ||
| 1372 | const auxFieldValue = (character.data && character.data[auxFieldName]) || ''; | ||
| 1373 | if (auxFieldValue) { | ||
| 1374 | template.find('.character_version').text(auxFieldValue); | ||
| 1375 | } | ||
| 1376 | else { | ||
| 1377 | template.find('.character_version').hide(); | ||
| 1378 | } | ||
| 1379 | |||
| 1371 | let queuePosition = groupChatQueueOrder.get(character.avatar); | 1380 | let queuePosition = groupChatQueueOrder.get(character.avatar); |
| 1372 | if (queuePosition) { | 1381 | if (queuePosition) { |
| 1373 | template.find('.queue_position').text(queuePosition); | 1382 | template.find('.queue_position').text(queuePosition); |
| @@ -7,7 +7,7 @@ | |||
| 7 | <li><span data-i18n="char_import_2">Chub Lorebook (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>lorebooks/bartleby/example-lorebook</tt></li> | 7 | <li><span data-i18n="char_import_2">Chub Lorebook (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>lorebooks/bartleby/example-lorebook</tt></li> |
| 8 | <li><span data-i18n="char_import_3">JanitorAI Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li> | 8 | <li><span data-i18n="char_import_3">JanitorAI Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li> |
| 9 | <li><span data-i18n="char_import_4">Pygmalion.chat Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li> | 9 | <li><span data-i18n="char_import_4">Pygmalion.chat Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li> |
| 10 | <li><span data-i18n="char_import_5">AICharacterCard.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li> | 10 | <li><span data-i18n="char_import_5">AICharacterCards.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li> |
| 11 | <li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li> | 11 | <li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li> |
| 12 | <li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li> | 12 | <li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li> |
| 13 | </ul> | 13 | </ul> |
| @@ -2928,7 +2928,7 @@ input[type=search]:focus::-webkit-search-cancel-button { | |||
| 2928 | position: relative; | 2928 | position: relative; |
| 2929 | } | 2929 | } |
| 2930 | 2930 | ||
| 2931 | #rm_print_characters_block .ch_name, | 2931 | .character_name_block .ch_name, |
| 2932 | .avatar-container .ch_name { | 2932 | .avatar-container .ch_name { |
| 2933 | flex: 1 1 auto; | 2933 | flex: 1 1 auto; |
| 2934 | white-space: nowrap; | 2934 | white-space: nowrap; |
| @@ -2938,6 +2938,13 @@ input[type=search]:focus::-webkit-search-cancel-button { | |||
| 2938 | display: block; | 2938 | display: block; |
| 2939 | } | 2939 | } |
| 2940 | 2940 | ||
| 2941 | .character_name_block .character_version { | ||
| 2942 | text-overflow: ellipsis; | ||
| 2943 | overflow: hidden; | ||
| 2944 | text-wrap: nowrap; | ||
| 2945 | max-width: 50%; | ||
| 2946 | } | ||
| 2947 | |||
| 2941 | #rm_print_characters_block .character_name_block> :last-child { | 2948 | #rm_print_characters_block .character_name_block> :last-child { |
| 2942 | flex: 0 100000 auto; | 2949 | flex: 0 100000 auto; |
| 2943 | /* Force shrinking first */ | 2950 | /* Force shrinking first */ |
| @@ -18,10 +18,9 @@ import { hideBin } from 'yargs/helpers'; | |||
| 18 | 18 | ||
| 19 | // express/server related library imports | 19 | // express/server related library imports |
| 20 | import cors from 'cors'; | 20 | import cors from 'cors'; |
| 21 | import { doubleCsrf } from 'csrf-csrf'; | 21 | import { csrfSync } from 'csrf-sync'; |
| 22 | import express from 'express'; | 22 | import express from 'express'; |
| 23 | import compression from 'compression'; | 23 | import compression from 'compression'; |
| 24 | import cookieParser from 'cookie-parser'; | ||
| 25 | import cookieSession from 'cookie-session'; | 24 | import cookieSession from 'cookie-session'; |
| 26 | import multer from 'multer'; | 25 | import multer from 'multer'; |
| 27 | import responseTime from 'response-time'; | 26 | import responseTime from 'response-time'; |
| @@ -40,7 +39,6 @@ util.inspect.defaultOptions.depth = 4; | |||
| 40 | import { loadPlugins } from './src/plugin-loader.js'; | 39 | import { loadPlugins } from './src/plugin-loader.js'; |
| 41 | import { | 40 | import { |
| 42 | initUserStorage, | 41 | initUserStorage, |
| 43 | getCsrfSecret, | ||
| 44 | getCookieSecret, | 42 | getCookieSecret, |
| 45 | getCookieSessionName, | 43 | getCookieSessionName, |
| 46 | getAllEnabledUsers, | 44 | getAllEnabledUsers, |
| @@ -67,6 +65,7 @@ import { | |||
| 67 | forwardFetchResponse, | 65 | forwardFetchResponse, |
| 68 | removeColorFormatting, | 66 | removeColorFormatting, |
| 69 | getSeparator, | 67 | getSeparator, |
| 68 | safeReadFileSync, | ||
| 70 | } from './src/util.js'; | 69 | } from './src/util.js'; |
| 71 | import { UPLOADS_DIRECTORY } from './src/constants.js'; | 70 | import { UPLOADS_DIRECTORY } from './src/constants.js'; |
| 72 | import { ensureThumbnailCache } from './src/endpoints/thumbnails.js'; | 71 | import { ensureThumbnailCache } from './src/endpoints/thumbnails.js'; |
| @@ -347,8 +346,8 @@ if (enableCorsProxy) { | |||
| 347 | } | 346 | } |
| 348 | 347 | ||
| 349 | function getSessionCookieAge() { | 348 | function getSessionCookieAge() { |
| 350 | // Defaults to 24 hours in seconds if not set | 349 | // Defaults to "no expiration" if not set |
| 351 | const configValue = getConfigValue('sessionTimeout', 24 * 60 * 60); | 350 | const configValue = getConfigValue('sessionTimeout', -1); |
| 352 | 351 | ||
| 353 | // Convert to milliseconds | 352 | // Convert to milliseconds |
| 354 | if (configValue > 0) { | 353 | if (configValue > 0) { |
| @@ -377,27 +376,38 @@ app.use(setUserDataMiddleware); | |||
| 377 | 376 | ||
| 378 | // CSRF Protection // | 377 | // CSRF Protection // |
| 379 | if (!disableCsrf) { | 378 | if (!disableCsrf) { |
| 380 | const COOKIES_SECRET = getCookieSecret(); | 379 | const csrfSyncProtection = csrfSync({ |
| 381 | 380 | getTokenFromState: (req) => { | |
| 382 | const { generateToken, doubleCsrfProtection } = doubleCsrf({ | 381 | if (!req.session) { |
| 383 | getSecret: getCsrfSecret, | 382 | console.error('(CSRF error) getTokenFromState: Session object not initialized'); |
| 384 | cookieName: 'X-CSRF-Token', | 383 | return; |
| 385 | cookieOptions: { | 384 | } |
| 386 | sameSite: 'strict', | 385 | return req.session.csrfToken; |
| 387 | secure: false, | 386 | }, |
| 387 | getTokenFromRequest: (req) => { | ||
| 388 | return req.headers['x-csrf-token']?.toString(); | ||
| 389 | }, | ||
| 390 | storeTokenInState: (req, token) => { | ||
| 391 | if (!req.session) { | ||
| 392 | console.error('(CSRF error) storeTokenInState: Session object not initialized'); | ||
| 393 | return; | ||
| 394 | } | ||
| 395 | req.session.csrfToken = token; | ||
| 388 | }, | 396 | }, |
| 389 | size: 64, | 397 | size: 32, |
| 390 | getTokenFromRequest: (req) => req.headers['x-csrf-token'], | ||
| 391 | }); | 398 | }); |
| 392 | 399 | ||
| 393 | app.get('/csrf-token', (req, res) => { | 400 | app.get('/csrf-token', (req, res) => { |
| 394 | res.json({ | 401 | res.json({ |
| 395 | 'token': generateToken(res, req), | 402 | 'token': csrfSyncProtection.generateToken(req), |
| 396 | }); | 403 | }); |
| 397 | }); | 404 | }); |
| 398 | 405 | ||
| 399 | app.use(cookieParser(COOKIES_SECRET)); | 406 | // Customize the error message |
| 400 | app.use(doubleCsrfProtection); | 407 | csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.'); |
| 408 | csrfSyncProtection.invalidCsrfTokenError.stack = undefined; | ||
| 409 | |||
| 410 | app.use(csrfSyncProtection.csrfSynchronisedProtection); | ||
| 401 | } else { | 411 | } else { |
| 402 | console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n'); | 412 | console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n'); |
| 403 | app.get('/csrf-token', (req, res) => { | 413 | app.get('/csrf-token', (req, res) => { |
| @@ -921,6 +931,16 @@ async function verifySecuritySettings() { | |||
| 921 | } | 931 | } |
| 922 | } | 932 | } |
| 923 | 933 | ||
| 934 | /** | ||
| 935 | * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered. | ||
| 936 | */ | ||
| 937 | function apply404Middleware() { | ||
| 938 | const notFoundWebpage = safeReadFileSync('./public/error/url-not-found.html') ?? ''; | ||
| 939 | app.use((req, res) => { | ||
| 940 | res.status(404).send(notFoundWebpage); | ||
| 941 | }); | ||
| 942 | } | ||
| 943 | |||
| 924 | // User storage module needs to be initialized before starting the server | 944 | // User storage module needs to be initialized before starting the server |
| 925 | initUserStorage(dataRoot) | 945 | initUserStorage(dataRoot) |
| 926 | .then(ensurePublicDirectoriesExist) | 946 | .then(ensurePublicDirectoriesExist) |
| @@ -928,4 +948,5 @@ initUserStorage(dataRoot) | |||
| 928 | .then(migrateSystemPrompts) | 948 | .then(migrateSystemPrompts) |
| 929 | .then(verifySecuritySettings) | 949 | .then(verifySecuritySettings) |
| 930 | .then(preSetupTasks) | 950 | .then(preSetupTasks) |
| 951 | .then(apply404Middleware) | ||
| 931 | .finally(startServer); | 952 | .finally(startServer); |
| @@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic'; | |||
| 9 | import { jsonParser, urlencodedParser } from '../express-common.js'; | 9 | import { jsonParser, urlencodedParser } from '../express-common.js'; |
| 10 | import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; | 10 | import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; |
| 11 | import { getImages, tryParse } from '../util.js'; | 11 | import { getImages, tryParse } from '../util.js'; |
| 12 | import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | ||
| 12 | 13 | ||
| 13 | export const router = express.Router(); | 14 | export const router = express.Router(); |
| 14 | 15 | ||
| @@ -17,7 +18,7 @@ router.post('/get', jsonParser, function (request, response) { | |||
| 17 | response.send(JSON.stringify(images)); | 18 | response.send(JSON.stringify(images)); |
| 18 | }); | 19 | }); |
| 19 | 20 | ||
| 20 | router.post('/delete', jsonParser, function (request, response) { | 21 | router.post('/delete', jsonParser, getFileNameValidationFunction('avatar'), function (request, response) { |
| 21 | if (!request.body) return response.sendStatus(400); | 22 | if (!request.body) return response.sendStatus(400); |
| 22 | 23 | ||
| 23 | if (request.body.avatar !== sanitize(request.body.avatar)) { | 24 | if (request.body.avatar !== sanitize(request.body.avatar)) { |
| @@ -37,6 +37,8 @@ import { | |||
| 37 | getTiktokenTokenizer, | 37 | getTiktokenTokenizer, |
| 38 | sentencepieceTokenizers, | 38 | sentencepieceTokenizers, |
| 39 | TEXT_COMPLETION_MODELS, | 39 | TEXT_COMPLETION_MODELS, |
| 40 | webTokenizers, | ||
| 41 | getWebTokenizer, | ||
| 40 | } from '../tokenizers.js'; | 42 | } from '../tokenizers.js'; |
| 41 | 43 | ||
| 42 | const API_OPENAI = 'https://api.openai.com/v1'; | 44 | const API_OPENAI = 'https://api.openai.com/v1'; |
| @@ -863,6 +865,14 @@ router.post('/bias', jsonParser, async function (request, response) { | |||
| 863 | return response.send({}); | 865 | return response.send({}); |
| 864 | } | 866 | } |
| 865 | encodeFunction = (text) => new Uint32Array(instance.encodeIds(text)); | 867 | encodeFunction = (text) => new Uint32Array(instance.encodeIds(text)); |
| 868 | } else if (webTokenizers.includes(model)) { | ||
| 869 | const tokenizer = getWebTokenizer(model); | ||
| 870 | const instance = await tokenizer?.get(); | ||
| 871 | if (!instance) { | ||
| 872 | console.warn('Tokenizer not initialized:', model); | ||
| 873 | return response.send({}); | ||
| 874 | } | ||
| 875 | encodeFunction = (text) => new Uint32Array(instance.encode(text)); | ||
| 866 | } else { | 876 | } else { |
| 867 | const tokenizer = getTiktokenTokenizer(model); | 877 | const tokenizer = getTiktokenTokenizer(model); |
| 868 | encodeFunction = (tokenizer.encode.bind(tokenizer)); | 878 | encodeFunction = (tokenizer.encode.bind(tokenizer)); |
| @@ -7,6 +7,7 @@ import sanitize from 'sanitize-filename'; | |||
| 7 | import { jsonParser, urlencodedParser } from '../express-common.js'; | 7 | import { jsonParser, urlencodedParser } from '../express-common.js'; |
| 8 | import { invalidateThumbnail } from './thumbnails.js'; | 8 | import { invalidateThumbnail } from './thumbnails.js'; |
| 9 | import { getImages } from '../util.js'; | 9 | import { getImages } from '../util.js'; |
| 10 | import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | ||
| 10 | 11 | ||
| 11 | export const router = express.Router(); | 12 | export const router = express.Router(); |
| 12 | 13 | ||
| @@ -15,7 +16,7 @@ router.post('/all', jsonParser, function (request, response) { | |||
| 15 | response.send(JSON.stringify(images)); | 16 | response.send(JSON.stringify(images)); |
| 16 | }); | 17 | }); |
| 17 | 18 | ||
| 18 | router.post('/delete', jsonParser, function (request, response) { | 19 | router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function (request, response) { |
| 19 | if (!request.body) return response.sendStatus(400); | 20 | if (!request.body) return response.sendStatus(400); |
| 20 | 21 | ||
| 21 | if (request.body.bg !== sanitize(request.body.bg)) { | 22 | if (request.body.bg !== sanitize(request.body.bg)) { |
| @@ -14,6 +14,7 @@ import jimp from 'jimp'; | |||
| 14 | 14 | ||
| 15 | import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; | 15 | import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; |
| 16 | import { jsonParser, urlencodedParser } from '../express-common.js'; | 16 | import { jsonParser, urlencodedParser } from '../express-common.js'; |
| 17 | import { default as validateAvatarUrlMiddleware, getFileNameValidationFunction } from '../middleware/validateFileName.js'; | ||
| 17 | import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js'; | 18 | import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js'; |
| 18 | import { TavernCardValidator } from '../validator/TavernCardValidator.js'; | 19 | import { TavernCardValidator } from '../validator/TavernCardValidator.js'; |
| 19 | import { parse, write } from '../character-card-parser.js'; | 20 | import { parse, write } from '../character-card-parser.js'; |
| @@ -73,12 +74,18 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u | |||
| 73 | * Read the image, resize, and save it as a PNG into the buffer. | 74 | * Read the image, resize, and save it as a PNG into the buffer. |
| 74 | * @returns {Promise<Buffer>} Image buffer | 75 | * @returns {Promise<Buffer>} Image buffer |
| 75 | */ | 76 | */ |
| 76 | function getInputImage() { | 77 | async function getInputImage() { |
| 78 | try { | ||
| 77 | if (Buffer.isBuffer(inputFile)) { | 79 | if (Buffer.isBuffer(inputFile)) { |
| 78 | return parseImageBuffer(inputFile, crop); | 80 | return await parseImageBuffer(inputFile, crop); |
| 79 | } | 81 | } |
| 80 | 82 | ||
| 81 | return tryReadImage(inputFile, crop); | 83 | return await tryReadImage(inputFile, crop); |
| 84 | } catch (error) { | ||
| 85 | const message = Buffer.isBuffer(inputFile) ? 'Failed to read image buffer.' : `Failed to read image: ${inputFile}.`; | ||
| 86 | console.warn(message, 'Using a fallback image.', error); | ||
| 87 | return await fs.promises.readFile(defaultAvatarPath); | ||
| 88 | } | ||
| 82 | } | 89 | } |
| 83 | 90 | ||
| 84 | const inputImage = await getInputImage(); | 91 | const inputImage = await getInputImage(); |
| @@ -756,7 +763,7 @@ router.post('/create', urlencodedParser, async function (request, response) { | |||
| 756 | } | 763 | } |
| 757 | }); | 764 | }); |
| 758 | 765 | ||
| 759 | router.post('/rename', jsonParser, async function (request, response) { | 766 | router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 760 | if (!request.body.avatar_url || !request.body.new_name) { | 767 | if (!request.body.avatar_url || !request.body.new_name) { |
| 761 | return response.sendStatus(400); | 768 | return response.sendStatus(400); |
| 762 | } | 769 | } |
| @@ -803,7 +810,7 @@ router.post('/rename', jsonParser, async function (request, response) { | |||
| 803 | } | 810 | } |
| 804 | }); | 811 | }); |
| 805 | 812 | ||
| 806 | router.post('/edit', urlencodedParser, async function (request, response) { | 813 | router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 807 | if (!request.body) { | 814 | if (!request.body) { |
| 808 | console.error('Error: no response body detected'); | 815 | console.error('Error: no response body detected'); |
| 809 | response.status(400).send('Error: no response body detected'); | 816 | response.status(400).send('Error: no response body detected'); |
| @@ -852,7 +859,7 @@ router.post('/edit', urlencodedParser, async function (request, response) { | |||
| 852 | * @param {Object} response - The HTTP response object. | 859 | * @param {Object} response - The HTTP response object. |
| 853 | * @returns {void} | 860 | * @returns {void} |
| 854 | */ | 861 | */ |
| 855 | router.post('/edit-attribute', jsonParser, async function (request, response) { | 862 | router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 856 | console.log(request.body); | 863 | console.log(request.body); |
| 857 | if (!request.body) { | 864 | if (!request.body) { |
| 858 | console.error('Error: no response body detected'); | 865 | console.error('Error: no response body detected'); |
| @@ -898,7 +905,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) { | |||
| 898 | * | 905 | * |
| 899 | * @returns {void} | 906 | * @returns {void} |
| 900 | * */ | 907 | * */ |
| 901 | router.post('/merge-attributes', jsonParser, async function (request, response) { | 908 | router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avatar'), async function (request, response) { |
| 902 | try { | 909 | try { |
| 903 | const update = request.body; | 910 | const update = request.body; |
| 904 | const avatarPath = path.join(request.user.directories.characters, update.avatar); | 911 | const avatarPath = path.join(request.user.directories.characters, update.avatar); |
| @@ -929,7 +936,7 @@ router.post('/merge-attributes', jsonParser, async function (request, response) | |||
| 929 | } | 936 | } |
| 930 | }); | 937 | }); |
| 931 | 938 | ||
| 932 | router.post('/delete', jsonParser, async function (request, response) { | 939 | router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 933 | if (!request.body || !request.body.avatar_url) { | 940 | if (!request.body || !request.body.avatar_url) { |
| 934 | return response.sendStatus(400); | 941 | return response.sendStatus(400); |
| 935 | } | 942 | } |
| @@ -992,7 +999,7 @@ router.post('/all', jsonParser, async function (request, response) { | |||
| 992 | } | 999 | } |
| 993 | }); | 1000 | }); |
| 994 | 1001 | ||
| 995 | router.post('/get', jsonParser, async function (request, response) { | 1002 | router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 996 | try { | 1003 | try { |
| 997 | if (!request.body) return response.sendStatus(400); | 1004 | if (!request.body) return response.sendStatus(400); |
| 998 | const item = request.body.avatar_url; | 1005 | const item = request.body.avatar_url; |
| @@ -1011,7 +1018,7 @@ router.post('/get', jsonParser, async function (request, response) { | |||
| 1011 | } | 1018 | } |
| 1012 | }); | 1019 | }); |
| 1013 | 1020 | ||
| 1014 | router.post('/chats', jsonParser, async function (request, response) { | 1021 | router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 1015 | if (!request.body) return response.sendStatus(400); | 1022 | if (!request.body) return response.sendStatus(400); |
| 1016 | 1023 | ||
| 1017 | const characterDirectory = (request.body.avatar_url).replace('.png', ''); | 1024 | const characterDirectory = (request.body.avatar_url).replace('.png', ''); |
| @@ -1160,7 +1167,7 @@ router.post('/import', urlencodedParser, async function (request, response) { | |||
| 1160 | } | 1167 | } |
| 1161 | }); | 1168 | }); |
| 1162 | 1169 | ||
| 1163 | router.post('/duplicate', jsonParser, async function (request, response) { | 1170 | router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 1164 | try { | 1171 | try { |
| 1165 | if (!request.body.avatar_url) { | 1172 | if (!request.body.avatar_url) { |
| 1166 | console.log('avatar URL not found in request body'); | 1173 | console.log('avatar URL not found in request body'); |
| @@ -1207,7 +1214,7 @@ router.post('/duplicate', jsonParser, async function (request, response) { | |||
| 1207 | } | 1214 | } |
| 1208 | }); | 1215 | }); |
| 1209 | 1216 | ||
| 1210 | router.post('/export', jsonParser, async function (request, response) { | 1217 | router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 1211 | try { | 1218 | try { |
| 1212 | if (!request.body.format || !request.body.avatar_url) { | 1219 | if (!request.body.format || !request.body.avatar_url) { |
| 1213 | return response.sendStatus(400); | 1220 | return response.sendStatus(400); |
| @@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic'; | |||
| 9 | import _ from 'lodash'; | 9 | import _ from 'lodash'; |
| 10 | 10 | ||
| 11 | import { jsonParser, urlencodedParser } from '../express-common.js'; | 11 | import { jsonParser, urlencodedParser } from '../express-common.js'; |
| 12 | import validateAvatarUrlMiddleware from '../middleware/validateFileName.js'; | ||
| 12 | import { | 13 | import { |
| 13 | getConfigValue, | 14 | getConfigValue, |
| 14 | humanizedISO8601DateTime, | 15 | humanizedISO8601DateTime, |
| @@ -294,7 +295,7 @@ function importRisuChat(userName, characterName, jsonData) { | |||
| 294 | 295 | ||
| 295 | export const router = express.Router(); | 296 | export const router = express.Router(); |
| 296 | 297 | ||
| 297 | router.post('/save', jsonParser, function (request, response) { | 298 | router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request, response) { |
| 298 | try { | 299 | try { |
| 299 | const directoryName = String(request.body.avatar_url).replace('.png', ''); | 300 | const directoryName = String(request.body.avatar_url).replace('.png', ''); |
| 300 | const chatData = request.body.chat; | 301 | const chatData = request.body.chat; |
| @@ -310,7 +311,7 @@ router.post('/save', jsonParser, function (request, response) { | |||
| 310 | } | 311 | } |
| 311 | }); | 312 | }); |
| 312 | 313 | ||
| 313 | router.post('/get', jsonParser, function (request, response) { | 314 | router.post('/get', jsonParser, validateAvatarUrlMiddleware, function (request, response) { |
| 314 | try { | 315 | try { |
| 315 | const dirName = String(request.body.avatar_url).replace('.png', ''); | 316 | const dirName = String(request.body.avatar_url).replace('.png', ''); |
| 316 | const directoryPath = path.join(request.user.directories.chats, dirName); | 317 | const directoryPath = path.join(request.user.directories.chats, dirName); |
| @@ -347,7 +348,7 @@ router.post('/get', jsonParser, function (request, response) { | |||
| 347 | }); | 348 | }); |
| 348 | 349 | ||
| 349 | 350 | ||
| 350 | router.post('/rename', jsonParser, async function (request, response) { | 351 | router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 351 | if (!request.body || !request.body.original_file || !request.body.renamed_file) { | 352 | if (!request.body || !request.body.original_file || !request.body.renamed_file) { |
| 352 | return response.sendStatus(400); | 353 | return response.sendStatus(400); |
| 353 | } | 354 | } |
| @@ -372,7 +373,7 @@ router.post('/rename', jsonParser, async function (request, response) { | |||
| 372 | return response.send({ ok: true, sanitizedFileName }); | 373 | return response.send({ ok: true, sanitizedFileName }); |
| 373 | }); | 374 | }); |
| 374 | 375 | ||
| 375 | router.post('/delete', jsonParser, function (request, response) { | 376 | router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (request, response) { |
| 376 | const dirName = String(request.body.avatar_url).replace('.png', ''); | 377 | const dirName = String(request.body.avatar_url).replace('.png', ''); |
| 377 | const fileName = String(request.body.chatfile); | 378 | const fileName = String(request.body.chatfile); |
| 378 | const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName)); | 379 | const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName)); |
| @@ -388,7 +389,7 @@ router.post('/delete', jsonParser, function (request, response) { | |||
| 388 | return response.send('ok'); | 389 | return response.send('ok'); |
| 389 | }); | 390 | }); |
| 390 | 391 | ||
| 391 | router.post('/export', jsonParser, async function (request, response) { | 392 | router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 392 | if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) { | 393 | if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) { |
| 393 | return response.sendStatus(400); | 394 | return response.sendStatus(400); |
| 394 | } | 395 | } |
| @@ -478,7 +479,7 @@ router.post('/group/import', urlencodedParser, function (request, response) { | |||
| 478 | } | 479 | } |
| 479 | }); | 480 | }); |
| 480 | 481 | ||
| 481 | router.post('/import', urlencodedParser, function (request, response) { | 482 | router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) { |
| 482 | if (!request.body) return response.sendStatus(400); | 483 | if (!request.body) return response.sendStatus(400); |
| 483 | 484 | ||
| 484 | const format = request.body.file_type; | 485 | const format = request.body.file_type; |
| @@ -626,7 +627,7 @@ router.post('/group/save', jsonParser, (request, response) => { | |||
| 626 | return response.send({ ok: true }); | 627 | return response.send({ ok: true }); |
| 627 | }); | 628 | }); |
| 628 | 629 | ||
| 629 | router.post('/search', jsonParser, function (request, response) { | 630 | router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (request, response) { |
| 630 | try { | 631 | try { |
| 631 | const { query, avatar_url, group_id } = request.body; | 632 | const { query, avatar_url, group_id } = request.body; |
| 632 | let chatFiles = []; | 633 | let chatFiles = []; |
| @@ -9,6 +9,7 @@ import { SETTINGS_FILE } from '../constants.js'; | |||
| 9 | import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js'; | 9 | import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js'; |
| 10 | import { jsonParser } from '../express-common.js'; | 10 | import { jsonParser } from '../express-common.js'; |
| 11 | import { getAllUserHandles, getUserDirectories } from '../users.js'; | 11 | import { getAllUserHandles, getUserDirectories } from '../users.js'; |
| 12 | import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | ||
| 12 | 13 | ||
| 13 | const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true); | 14 | const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true); |
| 14 | const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true); | 15 | const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true); |
| @@ -296,7 +297,7 @@ router.post('/get-snapshots', jsonParser, async (request, response) => { | |||
| 296 | } | 297 | } |
| 297 | }); | 298 | }); |
| 298 | 299 | ||
| 299 | router.post('/load-snapshot', jsonParser, async (request, response) => { | 300 | router.post('/load-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => { |
| 300 | try { | 301 | try { |
| 301 | const userFilesPattern = getFilePrefix(request.user.profile.handle); | 302 | const userFilesPattern = getFilePrefix(request.user.profile.handle); |
| 302 | 303 | ||
| @@ -330,7 +331,7 @@ router.post('/make-snapshot', jsonParser, async (request, response) => { | |||
| 330 | } | 331 | } |
| 331 | }); | 332 | }); |
| 332 | 333 | ||
| 333 | router.post('/restore-snapshot', jsonParser, async (request, response) => { | 334 | router.post('/restore-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => { |
| 334 | try { | 335 | try { |
| 335 | const userFilesPattern = getFilePrefix(request.user.profile.handle); | 336 | const userFilesPattern = getFilePrefix(request.user.profile.handle); |
| 336 | 337 | ||
| @@ -238,6 +238,15 @@ export const sentencepieceTokenizers = [ | |||
| 238 | 'jamba', | 238 | 'jamba', |
| 239 | ]; | 239 | ]; |
| 240 | 240 | ||
| 241 | export const webTokenizers = [ | ||
| 242 | 'claude', | ||
| 243 | 'llama3', | ||
| 244 | 'command-r', | ||
| 245 | 'qwen2', | ||
| 246 | 'nemo', | ||
| 247 | 'deepseek', | ||
| 248 | ]; | ||
| 249 | |||
| 241 | /** | 250 | /** |
| 242 | * Gets the Sentencepiece tokenizer by the model name. | 251 | * Gets the Sentencepiece tokenizer by the model name. |
| 243 | * @param {string} model Sentencepiece model name | 252 | * @param {string} model Sentencepiece model name |
| @@ -276,6 +285,39 @@ export function getSentencepiceTokenizer(model) { | |||
| 276 | } | 285 | } |
| 277 | 286 | ||
| 278 | /** | 287 | /** |
| 288 | * Gets the Web tokenizer by the model name. | ||
| 289 | * @param {string} model Web tokenizer model name | ||
| 290 | * @returns {WebTokenizer|null} Web tokenizer | ||
| 291 | */ | ||
| 292 | export function getWebTokenizer(model) { | ||
| 293 | if (model.includes('llama3')) { | ||
| 294 | return llama3_tokenizer; | ||
| 295 | } | ||
| 296 | |||
| 297 | if (model.includes('claude')) { | ||
| 298 | return claude_tokenizer; | ||
| 299 | } | ||
| 300 | |||
| 301 | if (model.includes('command-r')) { | ||
| 302 | return commandTokenizer; | ||
| 303 | } | ||
| 304 | |||
| 305 | if (model.includes('qwen2')) { | ||
| 306 | return qwen2Tokenizer; | ||
| 307 | } | ||
| 308 | |||
| 309 | if (model.includes('nemo')) { | ||
| 310 | return nemoTokenizer; | ||
| 311 | } | ||
| 312 | |||
| 313 | if (model.includes('deepseek')) { | ||
| 314 | return deepseekTokenizer; | ||
| 315 | } | ||
| 316 | |||
| 317 | return null; | ||
| 318 | } | ||
| 319 | |||
| 320 | /** | ||
| 279 | * Counts the token ids for the given text using the Sentencepiece tokenizer. | 321 | * Counts the token ids for the given text using the Sentencepiece tokenizer. |
| 280 | * @param {SentencePieceTokenizer} tokenizer Sentencepiece tokenizer | 322 | * @param {SentencePieceTokenizer} tokenizer Sentencepiece tokenizer |
| 281 | * @param {string} text Text to tokenize | 323 | * @param {string} text Text to tokenize |
| @@ -23,6 +23,7 @@ router.post('/logout', async (request, response) => { | |||
| 23 | } | 23 | } |
| 24 | 24 | ||
| 25 | request.session.handle = null; | 25 | request.session.handle = null; |
| 26 | request.session.csrfToken = null; | ||
| 26 | request.session = null; | 27 | request.session = null; |
| 27 | return response.sendStatus(204); | 28 | return response.sendStatus(204); |
| 28 | } catch (error) { | 29 | } catch (error) { |
| @@ -5,17 +5,18 @@ | |||
| 5 | import { Buffer } from 'node:buffer'; | 5 | import { Buffer } from 'node:buffer'; |
| 6 | import storage from 'node-persist'; | 6 | import storage from 'node-persist'; |
| 7 | import { getAllUserHandles, toKey, getPasswordHash } from '../users.js'; | 7 | import { getAllUserHandles, toKey, getPasswordHash } from '../users.js'; |
| 8 | import { getConfig, getConfigValue } from '../util.js'; | 8 | import { getConfig, getConfigValue, safeReadFileSync } from '../util.js'; |
| 9 | 9 | ||
| 10 | const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); | 10 | const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); |
| 11 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); | 11 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); |
| 12 | 12 | ||
| 13 | const basicAuthMiddleware = async function (request, response, callback) { | ||
| 14 | const unauthorizedWebpage = safeReadFileSync('./public/error/unauthorized.html') ?? ''; | ||
| 13 | const unauthorizedResponse = (res) => { | 15 | const unauthorizedResponse = (res) => { |
| 14 | res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"'); | 16 | res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"'); |
| 15 | return res.status(401).send('Authentication required'); | 17 | return res.status(401).send(unauthorizedWebpage); |
| 16 | }; | 18 | }; |
| 17 | 19 | ||
| 18 | const basicAuthMiddleware = async function (request, response, callback) { | ||
| 19 | const config = getConfig(); | 20 | const config = getConfig(); |
| 20 | const authHeader = request.headers.authorization; | 21 | const authHeader = request.headers.authorization; |
| 21 | 22 | ||
| @@ -0,0 +1,34 @@ | |||
| 1 | import path from 'node:path'; | ||
| 2 | |||
| 3 | /** | ||
| 4 | * Gets a middleware function that validates the field in the request body. | ||
| 5 | * @param {string} fieldName Field name | ||
| 6 | * @returns {import('express').RequestHandler} Middleware function | ||
| 7 | */ | ||
| 8 | export function getFileNameValidationFunction(fieldName) { | ||
| 9 | /** | ||
| 10 | * Validates the field in the request body. | ||
| 11 | * @param {import('express').Request} req Request object | ||
| 12 | * @param {import('express').Response} res Response object | ||
| 13 | * @param {import('express').NextFunction} next Next middleware | ||
| 14 | */ | ||
| 15 | return function validateAvatarUrlMiddleware(req, res, next) { | ||
| 16 | if (req.body && fieldName in req.body && typeof req.body[fieldName] === 'string') { | ||
| 17 | const forbiddenRegExp = path.sep === '/' ? /[/\x00]/ : /[/\x00\\]/; | ||
| 18 | if (forbiddenRegExp.test(req.body[fieldName])) { | ||
| 19 | console.error('An error occurred while validating the request body', { | ||
| 20 | handle: req.user.profile.handle, | ||
| 21 | path: req.originalUrl, | ||
| 22 | field: fieldName, | ||
| 23 | value: req.body[fieldName], | ||
| 24 | }); | ||
| 25 | return res.sendStatus(400); | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | next(); | ||
| 30 | }; | ||
| 31 | } | ||
| 32 | |||
| 33 | const avatarUrlValidationFunction = getFileNameValidationFunction('avatar_url'); | ||
| 34 | export default avatarUrlValidationFunction; | ||
| @@ -1,10 +1,11 @@ | |||
| 1 | import path from 'node:path'; | 1 | import path from 'node:path'; |
| 2 | import fs from 'node:fs'; | 2 | import fs from 'node:fs'; |
| 3 | import process from 'node:process'; | 3 | import process from 'node:process'; |
| 4 | import Handlebars from 'handlebars'; | ||
| 4 | import ipMatching from 'ip-matching'; | 5 | import ipMatching from 'ip-matching'; |
| 5 | 6 | ||
| 6 | import { getIpFromRequest } from '../express-common.js'; | 7 | import { getIpFromRequest } from '../express-common.js'; |
| 7 | import { color, getConfigValue } from '../util.js'; | 8 | import { color, getConfigValue, safeReadFileSync } from '../util.js'; |
| 8 | 9 | ||
| 9 | const whitelistPath = path.join(process.cwd(), './whitelist.txt'); | 10 | const whitelistPath = path.join(process.cwd(), './whitelist.txt'); |
| 10 | const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false); | 11 | const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false); |
| @@ -52,12 +53,16 @@ function getForwardedIp(req) { | |||
| 52 | * @returns {import('express').RequestHandler} The middleware function | 53 | * @returns {import('express').RequestHandler} The middleware function |
| 53 | */ | 54 | */ |
| 54 | export default function whitelistMiddleware(whitelistMode, listen) { | 55 | export default function whitelistMiddleware(whitelistMode, listen) { |
| 56 | const forbiddenWebpage = Handlebars.compile( | ||
| 57 | safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '', | ||
| 58 | ); | ||
| 59 | |||
| 55 | return function (req, res, next) { | 60 | return function (req, res, next) { |
| 56 | const clientIp = getIpFromRequest(req); | 61 | const clientIp = getIpFromRequest(req); |
| 57 | const forwardedIp = getForwardedIp(req); | 62 | const forwardedIp = getForwardedIp(req); |
| 63 | const userAgent = req.headers['user-agent']; | ||
| 58 | 64 | ||
| 59 | if (listen && !knownIPs.has(clientIp)) { | 65 | if (listen && !knownIPs.has(clientIp)) { |
| 60 | const userAgent = req.headers['user-agent']; | ||
| 61 | console.log(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`)); | 66 | console.log(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`)); |
| 62 | knownIPs.add(clientIp); | 67 | knownIPs.add(clientIp); |
| 63 | 68 | ||
| @@ -76,9 +81,15 @@ export default function whitelistMiddleware(whitelistMode, listen) { | |||
| 76 | || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x))) | 81 | || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x))) |
| 77 | ) { | 82 | ) { |
| 78 | // Log the connection attempt with real IP address | 83 | // Log the connection attempt with real IP address |
| 79 | const ipDetails = forwardedIp ? `${clientIp} (forwarded from ${forwardedIp})` : clientIp; | 84 | const ipDetails = forwardedIp |
| 80 | console.log(color.red('Forbidden: Connection attempt from ' + ipDetails + '. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.\n')); | 85 | ? `${clientIp} (forwarded from ${forwardedIp})` |
| 81 | return res.status(403).send('<b>Forbidden</b>: Connection attempt from <b>' + ipDetails + '</b>. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.'); | 86 | : clientIp; |
| 87 | console.log( | ||
| 88 | color.red( | ||
| 89 | `Blocked connection from ${clientIp}; User Agent: ${userAgent}\n\tTo allow this connection, add its IP address to the whitelist or disable whitelist mode by editing config.yaml in the root directory of your SillyTavern installation.\n`, | ||
| 90 | ), | ||
| 91 | ); | ||
| 92 | return res.status(403).send(forbiddenWebpage({ ipDetails })); | ||
| 82 | } | 93 | } |
| 83 | next(); | 94 | next(); |
| 84 | }; | 95 | }; |
| @@ -458,7 +458,8 @@ export function getPasswordSalt() { | |||
| 458 | */ | 458 | */ |
| 459 | export function getCookieSessionName() { | 459 | export function getCookieSessionName() { |
| 460 | // Get server hostname and hash it to generate a session suffix | 460 | // Get server hostname and hash it to generate a session suffix |
| 461 | const suffix = crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 8); | 461 | const hostname = os.hostname() || 'localhost'; |
| 462 | const suffix = crypto.createHash('sha256').update(hostname).digest('hex').slice(0, 8); | ||
| 462 | return `session-${suffix}`; | 463 | return `session-${suffix}`; |
| 463 | } | 464 | } |
| 464 | 465 | ||
| @@ -871,3 +871,14 @@ export class MemoryLimitedMap { | |||
| 871 | return this.map[Symbol.iterator](); | 871 | return this.map[Symbol.iterator](); |
| 872 | } | 872 | } |
| 873 | } | 873 | } |
| 874 | |||
| 875 | /** | ||
| 876 | * A 'safe' version of `fs.readFileSync()`. Returns the contents of a file if it exists, falling back to a default value if not. | ||
| 877 | * @param {string} filePath Path of the file to be read. | ||
| 878 | * @param {Parameters<typeof fs.readFileSync>[1]} options Options object to pass through to `fs.readFileSync()` (default: `{ encoding: 'utf-8' }`). | ||
| 879 | * @returns The contents at `filePath` if it exists, or `null` if not. | ||
| 880 | */ | ||
| 881 | export function safeReadFileSync(filePath, options = { encoding: 'utf-8' }) { | ||
| 882 | if (fs.existsSync(filePath)) return fs.readFileSync(filePath, options); | ||
| 883 | return null; | ||
| 884 | } | ||