Move cookie secret to data root. Make config.yaml immutable
| @@ -79,8 +79,6 @@ minLogLevel: 0 | |||
| 79 | ## Set to 0 to expire session when the browser is closed | 79 | ## Set to 0 to expire session when the browser is closed |
| 80 | ## Set to a negative number to disable session expiration | 80 | ## Set to a negative number to disable session expiration |
| 81 | sessionTimeout: -1 | 81 | sessionTimeout: -1 |
| 82 | # Used to sign session cookies. Will be auto-generated if not set | ||
| 83 | cookieSecret: '' | ||
| 84 | # Disable CSRF protection - NOT RECOMMENDED | 82 | # Disable CSRF protection - NOT RECOMMENDED |
| 85 | disableCsrfProtection: false | 83 | disableCsrfProtection: false |
| 86 | # Disable startup security checks - NOT RECOMMENDED | 84 | # Disable startup security checks - NOT RECOMMENDED |
| @@ -104,6 +104,15 @@ const keyMigrationMap = [ | |||
| 104 | newKey: 'extensions.models.textToSpeech', | 104 | newKey: 'extensions.models.textToSpeech', |
| 105 | migrate: (value) => value, | 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 | // Migrate old keys to new keys | 173 | // Migrate old keys to new keys |
| 165 | const migratedKeys = []; | 174 | const migratedKeys = []; |
| 166 | for (const { oldKey, newKey, migrate } of keyMigrationMap) { | 175 | for (const { oldKey, newKey, migrate, remove } of keyMigrationMap) { |
| 167 | if (_.has(config, oldKey)) { | 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 | const oldValue = _.get(config, oldKey); | 186 | const oldValue = _.get(config, oldKey); |
| 169 | const newValue = migrate(oldValue); | 187 | const newValue = migrate(oldValue); |
| 170 | _.set(config, newKey, newValue); | 188 | _.set(config, newKey, newValue); |
| @@ -274,7 +274,7 @@ const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('list | |||
| 274 | const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); | 274 | const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); |
| 275 | const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); | 275 | const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); |
| 276 | /** @type {string} */ | 276 | /** @type {string} */ |
| 277 | const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); | 277 | globalThis.DATA_ROOT = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); |
| 278 | /** @type {boolean} */ | 278 | /** @type {boolean} */ |
| 279 | const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); | 279 | const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); |
| 280 | const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); | 280 | const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); |
| @@ -282,7 +282,7 @@ const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BAS | |||
| 282 | /** @type {boolean} */ | 282 | /** @type {boolean} */ |
| 283 | const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); | 283 | const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); |
| 284 | 284 | ||
| 285 | const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY); | 285 | const uploadsPath = path.join(globalThis.DATA_ROOT, UPLOADS_DIRECTORY); |
| 286 | 286 | ||
| 287 | 287 | ||
| 288 | /** @type {boolean | "auto"} */ | 288 | /** @type {boolean | "auto"} */ |
| @@ -466,7 +466,7 @@ app.use(cookieSession({ | |||
| 466 | sameSite: 'strict', | 466 | sameSite: 'strict', |
| 467 | httpOnly: true, | 467 | httpOnly: true, |
| 468 | maxAge: getSessionCookieAge(), | 468 | maxAge: getSessionCookieAge(), |
| 469 | secret: getCookieSecret(), | 469 | secret: getCookieSecret(globalThis.DATA_ROOT), |
| 470 | })); | 470 | })); |
| 471 | 471 | ||
| 472 | app.use(setUserDataMiddleware); | 472 | app.use(setUserDataMiddleware); |
| @@ -1137,7 +1137,7 @@ function apply404Middleware() { | |||
| 1137 | } | 1137 | } |
| 1138 | 1138 | ||
| 1139 | // User storage module needs to be initialized before starting the server | 1139 | // User storage module needs to be initialized before starting the server |
| 1140 | initUserStorage(dataRoot) | 1140 | initUserStorage(globalThis.DATA_ROOT) |
| 1141 | .then(ensurePublicDirectoriesExist) | 1141 | .then(ensurePublicDirectoriesExist) |
| 1142 | .then(migrateUserData) | 1142 | .then(migrateUserData) |
| 1143 | .then(migrateSystemPrompts) | 1143 | .then(migrateSystemPrompts) |
| @@ -15,7 +15,7 @@ import _ from 'lodash'; | |||
| 15 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; | 15 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
| 16 | 16 | ||
| 17 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } from './constants.js'; | 17 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } from './constants.js'; |
| 18 | import { getConfigValue, color, delay, setConfigValue, generateTimestamp } from './util.js'; | 18 | import { getConfigValue, color, delay, generateTimestamp } from './util.js'; |
| 19 | import { readSecret, writeSecret } from './endpoints/secrets.js'; | 19 | import { readSecret, writeSecret } from './endpoints/secrets.js'; |
| 20 | import { getContentOfType } from './endpoints/content-manager.js'; | 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 | const DIRECTORIES_CACHE = new Map(); | 33 | const DIRECTORIES_CACHE = new Map(); |
| 34 | const PUBLIC_USER_AVATAR = '/img/default-user.png'; | 34 | const PUBLIC_USER_AVATAR = '/img/default-user.png'; |
| 35 | const COOKIE_SECRET_PATH = 'cookie-secret.txt'; | ||
| 35 | 36 | ||
| 36 | const STORAGE_KEYS = { | 37 | const STORAGE_KEYS = { |
| 37 | csrfSecret: 'csrfSecret', | 38 | csrfSecret: 'csrfSecret', |
| @@ -412,11 +413,10 @@ export function toAvatarKey(handle) { | |||
| 412 | * @returns {Promise<void>} | 413 | * @returns {Promise<void>} |
| 413 | */ | 414 | */ |
| 414 | export async function initUserStorage(dataRoot) { | 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 | console.log(); | 417 | console.log(); |
| 418 | await storage.init({ | 418 | await storage.init({ |
| 419 | dir: path.join(globalThis.DATA_ROOT, '_storage'), | 419 | dir: path.join(dataRoot, '_storage'), |
| 420 | ttl: false, // Never expire | 420 | ttl: false, // Never expire |
| 421 | }); | 421 | }); |
| 422 | 422 | ||
| @@ -430,17 +430,29 @@ export async function initUserStorage(dataRoot) { | |||
| 430 | 430 | ||
| 431 | /** | 431 | /** |
| 432 | * Get the cookie secret from the config. If it doesn't exist, generate a new one. | 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 | * @returns {string} The cookie secret | 434 | * @returns {string} The cookie secret |
| 434 | */ | 435 | */ |
| 435 | export function getCookieSecret() { | 436 | export function getCookieSecret(dataRoot) { |
| 436 | let secret = getConfigValue(STORAGE_KEYS.cookieSecret); | 437 | const cookieSecretPath = path.join(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 | return secret; | 456 | return secret; |
| 445 | } | 457 | } |
| 446 | 458 | ||
| @@ -9,7 +9,6 @@ import { promises as dnsPromise } from 'node:dns'; | |||
| 9 | 9 | ||
| 10 | import yaml from 'yaml'; | 10 | import yaml from 'yaml'; |
| 11 | import { sync as commandExistsSync } from 'command-exists'; | 11 | import { sync as commandExistsSync } from 'command-exists'; |
| 12 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; | ||
| 13 | import _ from 'lodash'; | 12 | import _ from 'lodash'; |
| 14 | import yauzl from 'yauzl'; | 13 | import yauzl from 'yauzl'; |
| 15 | import mime from 'mime-types'; | 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 | * Encodes the Basic Auth header value for the given user and password. | 61 | * Encodes the Basic Auth header value for the given user and password. |
| 76 | * @param {string} auth username:password | 62 | * @param {string} auth username:password |
| 77 | * @returns {string} Basic Auth header value | 63 | * @returns {string} Basic Auth header value |