Move cookie secret to data root. Make config.yaml immutable
| @@ -79,8 +79,6 @@ minLogLevel: 0 | ||
| 79 | 79 | ## Set to 0 to expire session when the browser is closed |
| 80 | 80 | ## Set to a negative number to disable session expiration |
| 81 | 81 | sessionTimeout: -1 |
| 82 | -# Used to sign session cookies. Will be auto-generated if not set | |
| 83 | -cookieSecret: '' | |
| 84 | 82 | # Disable CSRF protection - NOT RECOMMENDED |
| 85 | 83 | disableCsrfProtection: false |
| 86 | 84 | # Disable startup security checks - NOT RECOMMENDED |
| @@ -104,6 +104,15 @@ const keyMigrationMap = [ | ||
| 104 | 104 | newKey: 'extensions.models.textToSpeech', |
| 105 | 105 | migrate: (value) => value, |
| 106 | 106 | }, |
| 107 | + // uncommend one release after 1.12.13 | |
| 108 | + /* | |
| 109 | + { | |
| 110 | + oldKey: 'cookieSecret', | |
| 111 | + newKey: 'cookieSecret', | |
| 112 | + migrate: () => void 0, | |
| 113 | + remove: true, | |
| 114 | + }, | |
| 115 | + */ | |
| 107 | 116 | ]; |
| 108 | 117 | |
| 109 | 118 | /** |
| @@ -163,8 +172,17 @@ function addMissingConfigValues() { | ||
| 163 | 172 | |
| 164 | 173 | // Migrate old keys to new keys |
| 165 | 174 | const migratedKeys = []; |
| 166 | 175 | for (const { oldKey, newKey, migrate, remove } of keyMigrationMap) { |
| 167 | 176 | if (_.has(config, oldKey)) { |
| 177 | + if (remove) { | |
| 178 | + _.unset(config, oldKey); | |
| 179 | + migratedKeys.push({ | |
| 180 | + oldKey, | |
| 181 | + newValue: void 0, | |
| 182 | + }); | |
| 183 | + continue; | |
| 184 | + } | |
| 185 | + | |
| 168 | 186 | const oldValue = _.get(config, oldKey); |
| 169 | 187 | const newValue = migrate(oldValue); |
| 170 | 188 | _.set(config, newKey, newValue); |
| @@ -274,7 +274,7 @@ const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('list | ||
| 274 | 274 | const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); |
| 275 | 275 | const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); |
| 276 | 276 | /** @type {string} */ |
| 277 | 277 | const dataRootglobalThis.DATA_ROOT = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); |
| 278 | 278 | /** @type {boolean} */ |
| 279 | 279 | const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); |
| 280 | 280 | const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); |
| @@ -282,7 +282,7 @@ const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BAS | ||
| 282 | 282 | /** @type {boolean} */ |
| 283 | 283 | const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); |
| 284 | 284 | |
| 285 | 285 | const uploadsPath = path.join(dataRootglobalThis.DATA_ROOT, UPLOADS_DIRECTORY); |
| 286 | 286 | |
| 287 | 287 | |
| 288 | 288 | /** @type {boolean | "auto"} */ |
| @@ -466,7 +466,7 @@ app.use(cookieSession({ | ||
| 466 | 466 | sameSite: 'strict', |
| 467 | 467 | httpOnly: true, |
| 468 | 468 | maxAge: getSessionCookieAge(), |
| 469 | 469 | secret: getCookieSecret(globalThis.DATA_ROOT), |
| 470 | 470 | })); |
| 471 | 471 | |
| 472 | 472 | app.use(setUserDataMiddleware); |
| @@ -1137,7 +1137,7 @@ function apply404Middleware() { | ||
| 1137 | 1137 | } |
| 1138 | 1138 | |
| 1139 | 1139 | // User storage module needs to be initialized before starting the server |
| 1140 | 1140 | initUserStorage(dataRootglobalThis.DATA_ROOT) |
| 1141 | 1141 | .then(ensurePublicDirectoriesExist) |
| 1142 | 1142 | .then(migrateUserData) |
| 1143 | 1143 | .then(migrateSystemPrompts) |
| @@ -15,7 +15,7 @@ import _ from 'lodash'; | ||
| 15 | 15 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
| 16 | 16 | |
| 17 | 17 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } from './constants.js'; |
| 18 | 18 | import { getConfigValue, color, delay, setConfigValue, generateTimestamp } from './util.js'; |
| 19 | 19 | import { readSecret, writeSecret } from './endpoints/secrets.js'; |
| 20 | 20 | import { getContentOfType } from './endpoints/content-manager.js'; |
| 21 | 21 | |
| @@ -32,6 +32,7 @@ const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); | ||
| 32 | 32 | */ |
| 33 | 33 | const DIRECTORIES_CACHE = new Map(); |
| 34 | 34 | const PUBLIC_USER_AVATAR = '/img/default-user.png'; |
| 35 | +const COOKIE_SECRET_PATH = 'cookie-secret.txt'; | |
| 35 | 36 | |
| 36 | 37 | const STORAGE_KEYS = { |
| 37 | 38 | csrfSecret: 'csrfSecret', |
| @@ -412,11 +413,10 @@ export function toAvatarKey(handle) { | ||
| 412 | 413 | * @returns {Promise<void>} |
| 413 | 414 | */ |
| 414 | 415 | export async function initUserStorage(dataRoot) { |
| 415 | - globalThis.DATA_ROOT = dataRoot; | |
| 416 | + console.log('Using data root:', color.green(dataRoot)); | |
| 416 | - console.log('Using data root:', color.green(globalThis.DATA_ROOT)); | |
| 417 | 417 | console.log(); |
| 418 | 418 | await storage.init({ |
| 419 | 419 | dir: path.join(globalThis.DATA_ROOTdataRoot, '_storage'), |
| 420 | 420 | ttl: false, // Never expire |
| 421 | 421 | }); |
| 422 | 422 | |
| @@ -430,17 +430,29 @@ export async function initUserStorage(dataRoot) { | ||
| 430 | 430 | |
| 431 | 431 | /** |
| 432 | 432 | * Get the cookie secret from the config. If it doesn't exist, generate a new one. |
| 433 | + * @param {string} dataRoot The root directory for user data | |
| 433 | 434 | * @returns {string} The cookie secret |
| 434 | 435 | */ |
| 435 | 436 | export function getCookieSecret(dataRoot) { |
| 436 | 437 | letconst secretcookieSecretPath = getConfigValue(STORAGE_KEYSpath.cookieSecretjoin(dataRoot, COOKIE_SECRET_PATH); |
| 438 | + | |
| 439 | + if (fs.existsSync(cookieSecretPath)) { | |
| 440 | + const stat = fs.statSync(cookieSecretPath); | |
| 441 | + if (stat.size > 0) { | |
| 442 | + return fs.readFileSync(cookieSecretPath, 'utf8'); | |
| 443 | + } | |
| 444 | + } | |
| 437 | 445 | |
| 438 | - if (!secret) { | |
| 446 | + const oldSecret = getConfigValue(STORAGE_KEYS.cookieSecret); | |
| 439 | - console.warn(color.yellow('Cookie secret is missing from config.yaml. Generating a new one...')); | |
| 447 | + if (oldSecret) { | |
| 440 | - secret = crypto.randomBytes(64).toString('base64'); | |
| 448 | + console.log('Migrating cookie secret from config.yaml...'); | |
| 441 | - setConfigValue(STORAGE_KEYS.cookieSecret, secret); | |
| 449 | + writeFileAtomicSync(cookieSecretPath, oldSecret, { encoding: 'utf8' }); | |
| 450 | + return oldSecret; | |
| 442 | 451 | } |
| 443 | 452 | |
| 453 | + console.warn(color.yellow('Cookie secret is missing from data root. Generating a new one...')); | |
| 454 | + const secret = crypto.randomBytes(64).toString('base64'); | |
| 455 | + writeFileAtomicSync(cookieSecretPath, secret, { encoding: 'utf8' }); | |
| 444 | 456 | return secret; |
| 445 | 457 | } |
| 446 | 458 | |
| @@ -9,7 +9,6 @@ import { promises as dnsPromise } from 'node:dns'; | ||
| 9 | 9 | |
| 10 | 10 | import yaml from 'yaml'; |
| 11 | 11 | import { sync as commandExistsSync } from 'command-exists'; |
| 12 | -import { sync as writeFileAtomicSync } from 'write-file-atomic'; | |
| 13 | 12 | import _ from 'lodash'; |
| 14 | 13 | import yauzl from 'yauzl'; |
| 15 | 14 | import mime from 'mime-types'; |
| @@ -59,19 +58,6 @@ export function getConfigValue(key, defaultValue = null) { | ||
| 59 | 58 | } |
| 60 | 59 | |
| 61 | 60 | /** |
| 62 | - * Sets a value for the given key in the config object and writes it to the config.yaml file. | |
| 63 | - * @param {string} key Key to set | |
| 64 | - * @param {any} value Value to set | |
| 65 | - */ | |
| 66 | -export function setConfigValue(key, value) { | |
| 67 | - // Reset cache so that the next getConfig call will read the updated config file | |
| 68 | - CACHED_CONFIG = null; | |
| 69 | - const config = getConfig(); | |
| 70 | - _.set(config, key, value); | |
| 71 | - writeFileAtomicSync('./config.yaml', yaml.stringify(config)); | |
| 72 | -} | |
| 73 | - | |
| 74 | -/** | |
| 75 | 61 | * Encodes the Basic Auth header value for the given user and password. |
| 76 | 62 | * @param {string} auth username:password |
| 77 | 63 | * @returns {string} Basic Auth header value |