From 7ea2c5f8cff0648be26f14eb6d68e9cd85a91678 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 20 Feb 2025 20:16:44 +0200 Subject: [PATCH] Move cookie secret to data root. Make config.yaml immutable --- default/config.yaml | 2 -- post-install.js | 20 +++++++++++++++++++- server.js | 8 ++++---- src/users.js | 32 ++++++++++++++++++++++---------- src/util.js | 14 -------------- 5 files changed, 45 insertions(+), 31 deletions(-) diff --git a/default/config.yaml b/default/config.yaml index 5d7597a6c..d87537ad5 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -79,8 +79,6 @@ minLogLevel: 0 ## Set to 0 to expire session when the browser is closed ## Set to a negative number to disable session expiration sessionTimeout: -1 -# Used to sign session cookies. Will be auto-generated if not set -cookieSecret: '' # Disable CSRF protection - NOT RECOMMENDED disableCsrfProtection: false # Disable startup security checks - NOT RECOMMENDED diff --git a/post-install.js b/post-install.js index 46ad160cb..50c60e9e4 100644 --- a/post-install.js +++ b/post-install.js @@ -104,6 +104,15 @@ const keyMigrationMap = [ newKey: 'extensions.models.textToSpeech', migrate: (value) => value, }, + // uncommend one release after 1.12.13 + /* + { + oldKey: 'cookieSecret', + newKey: 'cookieSecret', + migrate: () => void 0, + remove: true, + }, + */ ]; /** @@ -163,8 +172,17 @@ function addMissingConfigValues() { // Migrate old keys to new keys const migratedKeys = []; - for (const { oldKey, newKey, migrate } of keyMigrationMap) { + for (const { oldKey, newKey, migrate, remove } of keyMigrationMap) { if (_.has(config, oldKey)) { + if (remove) { + _.unset(config, oldKey); + migratedKeys.push({ + oldKey, + newValue: void 0, + }); + continue; + } + const oldValue = _.get(config, oldKey); const newValue = migrate(oldValue); _.set(config, newKey, newValue); diff --git a/server.js b/server.js index b83a79235..cdef0a64b 100644 --- a/server.js +++ b/server.js @@ -274,7 +274,7 @@ const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('list const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); /** @type {string} */ -const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); +globalThis.DATA_ROOT = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); /** @type {boolean} */ const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); @@ -282,7 +282,7 @@ const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BAS /** @type {boolean} */ const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); -const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY); +const uploadsPath = path.join(globalThis.DATA_ROOT, UPLOADS_DIRECTORY); /** @type {boolean | "auto"} */ @@ -466,7 +466,7 @@ app.use(cookieSession({ sameSite: 'strict', httpOnly: true, maxAge: getSessionCookieAge(), - secret: getCookieSecret(), + secret: getCookieSecret(globalThis.DATA_ROOT), })); app.use(setUserDataMiddleware); @@ -1137,7 +1137,7 @@ function apply404Middleware() { } // User storage module needs to be initialized before starting the server -initUserStorage(dataRoot) +initUserStorage(globalThis.DATA_ROOT) .then(ensurePublicDirectoriesExist) .then(migrateUserData) .then(migrateSystemPrompts) diff --git a/src/users.js b/src/users.js index 2d7641ff2..7de379f25 100644 --- a/src/users.js +++ b/src/users.js @@ -15,7 +15,7 @@ import _ from 'lodash'; import { sync as writeFileAtomicSync } from 'write-file-atomic'; import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } from './constants.js'; -import { getConfigValue, color, delay, setConfigValue, generateTimestamp } from './util.js'; +import { getConfigValue, color, delay, generateTimestamp } from './util.js'; import { readSecret, writeSecret } from './endpoints/secrets.js'; import { getContentOfType } from './endpoints/content-manager.js'; @@ -32,6 +32,7 @@ const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); */ const DIRECTORIES_CACHE = new Map(); const PUBLIC_USER_AVATAR = '/img/default-user.png'; +const COOKIE_SECRET_PATH = 'cookie-secret.txt'; const STORAGE_KEYS = { csrfSecret: 'csrfSecret', @@ -412,11 +413,10 @@ export function toAvatarKey(handle) { * @returns {Promise} */ export async function initUserStorage(dataRoot) { - globalThis.DATA_ROOT = dataRoot; - console.log('Using data root:', color.green(globalThis.DATA_ROOT)); + console.log('Using data root:', color.green(dataRoot)); console.log(); await storage.init({ - dir: path.join(globalThis.DATA_ROOT, '_storage'), + dir: path.join(dataRoot, '_storage'), ttl: false, // Never expire }); @@ -430,17 +430,29 @@ export async function initUserStorage(dataRoot) { /** * Get the cookie secret from the config. If it doesn't exist, generate a new one. + * @param {string} dataRoot The root directory for user data * @returns {string} The cookie secret */ -export function getCookieSecret() { - let secret = getConfigValue(STORAGE_KEYS.cookieSecret); +export function getCookieSecret(dataRoot) { + const cookieSecretPath = path.join(dataRoot, COOKIE_SECRET_PATH); - if (!secret) { - console.warn(color.yellow('Cookie secret is missing from config.yaml. Generating a new one...')); - secret = crypto.randomBytes(64).toString('base64'); - setConfigValue(STORAGE_KEYS.cookieSecret, secret); + if (fs.existsSync(cookieSecretPath)) { + const stat = fs.statSync(cookieSecretPath); + if (stat.size > 0) { + return fs.readFileSync(cookieSecretPath, 'utf8'); + } } + const oldSecret = getConfigValue(STORAGE_KEYS.cookieSecret); + if (oldSecret) { + console.log('Migrating cookie secret from config.yaml...'); + writeFileAtomicSync(cookieSecretPath, oldSecret, { encoding: 'utf8' }); + return oldSecret; + } + + console.warn(color.yellow('Cookie secret is missing from data root. Generating a new one...')); + const secret = crypto.randomBytes(64).toString('base64'); + writeFileAtomicSync(cookieSecretPath, secret, { encoding: 'utf8' }); return secret; } diff --git a/src/util.js b/src/util.js index 426b238ce..0ce6f66d4 100644 --- a/src/util.js +++ b/src/util.js @@ -9,7 +9,6 @@ import { promises as dnsPromise } from 'node:dns'; import yaml from 'yaml'; import { sync as commandExistsSync } from 'command-exists'; -import { sync as writeFileAtomicSync } from 'write-file-atomic'; import _ from 'lodash'; import yauzl from 'yauzl'; import mime from 'mime-types'; @@ -58,19 +57,6 @@ export function getConfigValue(key, defaultValue = null) { return _.get(config, key, defaultValue); } -/** - * Sets a value for the given key in the config object and writes it to the config.yaml file. - * @param {string} key Key to set - * @param {any} value Value to set - */ -export function setConfigValue(key, value) { - // Reset cache so that the next getConfig call will read the updated config file - CACHED_CONFIG = null; - const config = getConfig(); - _.set(config, key, value); - writeFileAtomicSync('./config.yaml', yaml.stringify(config)); -} - /** * Encodes the Basic Auth header value for the given user and password. * @param {string} auth username:password