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

7ea2c5f8cff0648be26f14eb6d68e9cd85a91678

Cohee <18619528+Cohee1207@users.noreply.github.com>

5 files changed, +45 -31Ignore whitespace
default/config.yaml+0 -2
@@ -79,8 +79,6 @@ minLogLevel: 0
79## Set to 0 to expire session when the browser is closed79## Set to 0 to expire session when the browser is closed
80## Set to a negative number to disable session expiration80## Set to a negative number to disable session expiration
81sessionTimeout: -181sessionTimeout: -1
82# Used to sign session cookies. Will be auto-generated if not set
83cookieSecret: ''
84# Disable CSRF protection - NOT RECOMMENDED82# Disable CSRF protection - NOT RECOMMENDED
85disableCsrfProtection: false83disableCsrfProtection: false
86# Disable startup security checks - NOT RECOMMENDED84# Disable startup security checks - NOT RECOMMENDED
post-install.js+19 -1
@@ -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];
108117
109/**118/**
@@ -163,8 +172,17 @@ function addMissingConfigValues() {
163172
164 // Migrate old keys to new keys173 // 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);
server.js+4 -4
@@ -274,7 +274,7 @@ const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('list
274const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);274const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
275const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);275const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);
276/** @type {string} */276/** @type {string} */
277const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');277globalThis.DATA_ROOT = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
278/** @type {boolean} */278/** @type {boolean} */
279const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);279const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
280const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);280const 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} */
283const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);283const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
284284
285const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);285const uploadsPath = path.join(globalThis.DATA_ROOT, UPLOADS_DIRECTORY);
286286
287287
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}));
471471
472app.use(setUserDataMiddleware);472app.use(setUserDataMiddleware);
@@ -1137,7 +1137,7 @@ function apply404Middleware() {
1137}1137}
11381138
1139// User storage module needs to be initialized before starting the server1139// User storage module needs to be initialized before starting the server
1140initUserStorage(dataRoot)1140initUserStorage(globalThis.DATA_ROOT)
1141 .then(ensurePublicDirectoriesExist)1141 .then(ensurePublicDirectoriesExist)
1142 .then(migrateUserData)1142 .then(migrateUserData)
1143 .then(migrateSystemPrompts)1143 .then(migrateSystemPrompts)
src/users.js+22 -10
@@ -15,7 +15,7 @@ import _ from 'lodash';
15import { sync as writeFileAtomicSync } from 'write-file-atomic';15import { sync as writeFileAtomicSync } from 'write-file-atomic';
1616
17import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } from './constants.js';17import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } from './constants.js';
18import { getConfigValue, color, delay, setConfigValue, generateTimestamp } from './util.js';18import { getConfigValue, color, delay, generateTimestamp } from './util.js';
19import { readSecret, writeSecret } from './endpoints/secrets.js';19import { readSecret, writeSecret } from './endpoints/secrets.js';
20import { getContentOfType } from './endpoints/content-manager.js';20import { getContentOfType } from './endpoints/content-manager.js';
2121
@@ -32,6 +32,7 @@ const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
32 */32 */
33const DIRECTORIES_CACHE = new Map();33const DIRECTORIES_CACHE = new Map();
34const PUBLIC_USER_AVATAR = '/img/default-user.png';34const PUBLIC_USER_AVATAR = '/img/default-user.png';
35const COOKIE_SECRET_PATH = 'cookie-secret.txt';
3536
36const STORAGE_KEYS = {37const 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 */
414export async function initUserStorage(dataRoot) {415export 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 expire420 ttl: false, // Never expire
421 });421 });
422422
@@ -430,17 +430,29 @@ export async function initUserStorage(dataRoot) {
430430
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 secret434 * @returns {string} The cookie secret
434 */435 */
435export function getCookieSecret() {436export 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 }
437445
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 }
443452
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}
446458
src/util.js+0 -14
@@ -9,7 +9,6 @@ import { promises as dnsPromise } from 'node:dns';
99
10import yaml from 'yaml';10import yaml from 'yaml';
11import { sync as commandExistsSync } from 'command-exists';11import { sync as commandExistsSync } from 'command-exists';
12import { sync as writeFileAtomicSync } from 'write-file-atomic';
13import _ from 'lodash';12import _ from 'lodash';
14import yauzl from 'yauzl';13import yauzl from 'yauzl';
15import mime from 'mime-types';14import mime from 'mime-types';
@@ -59,19 +58,6 @@ export function getConfigValue(key, defaultValue = null) {
59}58}
6059
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 */
66export 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:password62 * @param {string} auth username:password
77 * @returns {string} Basic Auth header value63 * @returns {string} Basic Auth header value