Move cookie secret to data root. Make config.yaml immutable

This commit is contained in:
Cohee 2025-02-20 20:16:44 +02:00
parent 3bb8b887e1
commit 7ea2c5f8cf
5 changed files with 45 additions and 31 deletions

View File

@ -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

View File

@ -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);

View File

@ -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)

View File

@ -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<void>}
*/
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;
}

View File

@ -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