'use strict'; exports = module.exports = { getAppAutoupdatePattern: getAppAutoupdatePattern, setAppAutoupdatePattern: setAppAutoupdatePattern, getBoxAutoupdatePattern: getBoxAutoupdatePattern, setBoxAutoupdatePattern: setBoxAutoupdatePattern, getTimeZone: getTimeZone, setTimeZone: setTimeZone, getCloudronName: getCloudronName, setCloudronName: setCloudronName, getCloudronAvatar: getCloudronAvatar, setCloudronAvatar: setCloudronAvatar, getDynamicDnsConfig: getDynamicDnsConfig, setDynamicDnsConfig: setDynamicDnsConfig, getUnstableAppsConfig: getUnstableAppsConfig, setUnstableAppsConfig: setUnstableAppsConfig, getBackupConfig: getBackupConfig, setBackupConfig: setBackupConfig, getPlatformConfig: getPlatformConfig, setPlatformConfig: setPlatformConfig, getExternalLdapConfig: getExternalLdapConfig, setExternalLdapConfig: setExternalLdapConfig, getRegistryConfig: getRegistryConfig, setRegistryConfig: setRegistryConfig, getLicenseKey: getLicenseKey, setLicenseKey: setLicenseKey, getCloudronId: getCloudronId, setCloudronId: setCloudronId, getCloudronToken: getCloudronToken, setCloudronToken: setCloudronToken, getAll: getAll, initCache: initCache, // these values come from the cache apiServerOrigin: apiServerOrigin, webServerOrigin: webServerOrigin, adminDomain: adminDomain, setAdmin: setAdmin, // these values are derived adminOrigin: adminOrigin, adminFqdn: adminFqdn, mailFqdn: mailFqdn, isDemo: isDemo, // booleans. if you add an entry here, be sure to fix getAll DYNAMIC_DNS_KEY: 'dynamic_dns', UNSTABLE_APPS_KEY: 'unstable_apps', DEMO_KEY: 'demo', // json. if you add an entry here, be sure to fix getAll BACKUP_CONFIG_KEY: 'backup_config', PLATFORM_CONFIG_KEY: 'platform_config', EXTERNAL_LDAP_KEY: 'external_ldap_config', REGISTRY_CONFIG_KEY: 'registry_config', // strings APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern', BOX_AUTOUPDATE_PATTERN_KEY: 'box_autoupdate_pattern', TIME_ZONE_KEY: 'time_zone', CLOUDRON_NAME_KEY: 'cloudron_name', LICENSE_KEY: 'license_key', CLOUDRON_ID_KEY: 'cloudron_id', CLOUDRON_TOKEN_KEY: 'cloudron_token', API_SERVER_ORIGIN_KEY: 'api_server_origin', WEB_SERVER_ORIGIN_KEY: 'web_server_origin', ADMIN_DOMAIN_KEY: 'admin_domain', ADMIN_FQDN_KEY: 'admin_fqdn', PROVIDER_KEY: 'provider', // blobs CLOUDRON_AVATAR_KEY: 'cloudron_avatar', // not stored in db but can be used for locked flag // testing _setApiServerOrigin: setApiServerOrigin }; var addons = require('./addons.js'), assert = require('assert'), backups = require('./backups.js'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), cron = require('./cron.js'), CronJob = require('cron').CronJob, debug = require('debug')('box:settings'), docker = require('./docker.js'), externalLdap = require('./externalldap.js'), moment = require('moment-timezone'), paths = require('./paths.js'), safe = require('safetydance'), settingsdb = require('./settingsdb.js'), util = require('util'), _ = require('underscore'); let gDefaults = (function () { var result = { }; result[exports.APP_AUTOUPDATE_PATTERN_KEY] = '00 30 1,3,5,23 * * *'; result[exports.BOX_AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *'; result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles'; result[exports.CLOUDRON_NAME_KEY] = 'Cloudron'; result[exports.DYNAMIC_DNS_KEY] = false; result[exports.UNSTABLE_APPS_KEY] = false; result[exports.LICENSE_KEY] = ''; result[exports.CLOUDRON_ID_KEY] = ''; result[exports.CLOUDRON_TOKEN_KEY] = ''; result[exports.BACKUP_CONFIG_KEY] = { provider: 'filesystem', key: '', backupFolder: '/var/backups', format: 'tgz', retentionSecs: 2 * 24 * 60 * 60, // 2 days intervalSecs: 24 * 60 * 60 // ~1 day }; result[exports.PLATFORM_CONFIG_KEY] = {}; result[exports.EXTERNAL_LDAP_KEY] = { provider: 'noop' }; result[exports.REGISTRY_CONFIG_KEY] = {}; result[exports.ADMIN_DOMAIN_KEY] = ''; result[exports.ADMIN_FQDN_KEY] = ''; result[exports.API_SERVER_ORIGIN_KEY] = 'https://api.cloudron.io'; result[exports.WEB_SERVER_ORIGIN_KEY] = 'https://cloudron.io'; result[exports.DEMO_KEY] = false; return result; })(); let gCache = {}; function notifyChange(key, value) { assert.strictEqual(typeof key, 'string'); // value is a variant cron.handleSettingsChanged(key, value); } function setAppAutoupdatePattern(pattern, callback) { assert.strictEqual(typeof pattern, 'string'); assert.strictEqual(typeof callback, 'function'); if (pattern !== constants.AUTOUPDATE_PATTERN_NEVER) { // check if pattern is valid var job = safe.safeCall(function () { return new CronJob(pattern); }); if (!job) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid pattern', { field: 'pattern' })); } settingsdb.set(exports.APP_AUTOUPDATE_PATTERN_KEY, pattern, function (error) { if (error) return callback(error); notifyChange(exports.APP_AUTOUPDATE_PATTERN_KEY, pattern); return callback(null); }); } function getAppAutoupdatePattern(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.APP_AUTOUPDATE_PATTERN_KEY, function (error, pattern) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.APP_AUTOUPDATE_PATTERN_KEY]); if (error) return callback(error); callback(null, pattern); }); } function setBoxAutoupdatePattern(pattern, callback) { assert.strictEqual(typeof pattern, 'string'); assert.strictEqual(typeof callback, 'function'); if (pattern !== constants.AUTOUPDATE_PATTERN_NEVER) { // check if pattern is valid var job = safe.safeCall(function () { return new CronJob(pattern); }); if (!job) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid pattern', { field: 'pattern' })); } settingsdb.set(exports.BOX_AUTOUPDATE_PATTERN_KEY, pattern, function (error) { if (error) return callback(error); notifyChange(exports.BOX_AUTOUPDATE_PATTERN_KEY, pattern); return callback(null); }); } function getBoxAutoupdatePattern(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.BOX_AUTOUPDATE_PATTERN_KEY, function (error, pattern) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.BOX_AUTOUPDATE_PATTERN_KEY]); if (error) return callback(error); callback(null, pattern); }); } function setTimeZone(tz, callback) { assert.strictEqual(typeof tz, 'string'); assert.strictEqual(typeof callback, 'function'); if (moment.tz.names().indexOf(tz) === -1) return callback(new BoxError(BoxError.BAD_FIELD, 'Bad timeZone', { field: 'timezone' })); settingsdb.set(exports.TIME_ZONE_KEY, tz, function (error) { if (error) return callback(error); notifyChange(exports.TIME_ZONE_KEY, tz); return callback(null); }); } function getTimeZone(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.TIME_ZONE_KEY, function (error, tz) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.TIME_ZONE_KEY]); if (error) return callback(error); callback(null, tz); }); } function getCloudronName(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.CLOUDRON_NAME_KEY, function (error, name) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_NAME_KEY]); if (error) return callback(error); callback(null, name); }); } function setCloudronName(name, callback) { assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof callback, 'function'); if (!name) return callback(new BoxError(BoxError.BAD_FIELD, 'name is empty', { field: 'name' })); // some arbitrary restrictions (for sake of ui layout) if (name.length > 32) return callback(new BoxError(BoxError.BAD_FIELD, 'name cannot exceed 32 characters', { field: 'name' })); settingsdb.set(exports.CLOUDRON_NAME_KEY, name, function (error) { if (error) return callback(error); notifyChange(exports.CLOUDRON_NAME_KEY, name); return callback(null); }); } function getCloudronAvatar(callback) { assert.strictEqual(typeof callback, 'function'); var avatar = safe.fs.readFileSync(paths.CLOUDRON_AVATAR_FILE); if (avatar) return callback(null, avatar); // try default fallback avatar = safe.fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE); if (avatar) return callback(null, avatar); callback(new BoxError(BoxError.FS_ERROR, safe.error)); } function setCloudronAvatar(avatar, callback) { assert(util.isBuffer(avatar)); assert.strictEqual(typeof callback, 'function'); if (!safe.fs.writeFileSync(paths.CLOUDRON_AVATAR_FILE, avatar)) { return callback(new BoxError(BoxError.FS_ERROR, safe.error)); } return callback(null); } function getDynamicDnsConfig(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.DYNAMIC_DNS_KEY, function (error, enabled) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.DYNAMIC_DNS_KEY]); if (error) return callback(error); callback(null, !!enabled); // settingsdb holds string values only }); } function setDynamicDnsConfig(enabled, callback) { assert.strictEqual(typeof enabled, 'boolean'); assert.strictEqual(typeof callback, 'function'); // settingsdb takes string values only settingsdb.set(exports.DYNAMIC_DNS_KEY, enabled ? 'enabled' : '', function (error) { if (error) return callback(error); notifyChange(exports.DYNAMIC_DNS_KEY, enabled); return callback(null); }); } function getUnstableAppsConfig(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.UNSTABLE_APPS_KEY, function (error, enabled) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.UNSTABLE_APPS_KEY]); if (error) return callback(error); callback(null, !!enabled); // settingsdb holds string values only }); } function setUnstableAppsConfig(enabled, callback) { assert.strictEqual(typeof enabled, 'boolean'); assert.strictEqual(typeof callback, 'function'); // settingsdb takes string values only settingsdb.set(exports.UNSTABLE_APPS_KEY, enabled ? 'enabled' : '', function (error) { if (error) return callback(error); notifyChange(exports.UNSTABLE_APPS_KEY, enabled); return callback(null); }); } function getBackupConfig(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.BACKUP_CONFIG_KEY, function (error, value) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.BACKUP_CONFIG_KEY]); if (error) return callback(error); callback(null, JSON.parse(value)); // provider, token, key, region, prefix, bucket }); } function setBackupConfig(backupConfig, callback) { assert.strictEqual(typeof backupConfig, 'object'); assert.strictEqual(typeof callback, 'function'); getBackupConfig(function (error, curentConfig) { if (error) return callback(error); backups.injectPrivateFields(backupConfig, curentConfig); backups.testConfig(backupConfig, function (error) { if (error) return callback(error); backups.cleanupCacheFilesSync(); settingsdb.set(exports.BACKUP_CONFIG_KEY, JSON.stringify(backupConfig), function (error) { if (error) return callback(error); notifyChange(exports.BACKUP_CONFIG_KEY, backupConfig); callback(null); }); }); }); } function getPlatformConfig(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.PLATFORM_CONFIG_KEY, function (error, value) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.PLATFORM_CONFIG_KEY]); if (error) return callback(error); callback(null, JSON.parse(value)); }); } function setPlatformConfig(platformConfig, callback) { assert.strictEqual(typeof callback, 'function'); for (let addon of [ 'mysql', 'postgresql', 'mail', 'mongodb' ]) { if (!platformConfig[addon]) continue; if (platformConfig[addon].memorySwap < platformConfig[addon].memory) return callback(new BoxError(BoxError.BAD_FIELD, 'memorySwap must be larger than memory', { field: 'memory', addon })); } settingsdb.set(exports.PLATFORM_CONFIG_KEY, JSON.stringify(platformConfig), function (error) { if (error) return callback(error); addons.updateServiceConfig(platformConfig, callback); }); } function getExternalLdapConfig(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.EXTERNAL_LDAP_KEY, function (error, value) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.EXTERNAL_LDAP_KEY]); if (error) return callback(error); callback(null, JSON.parse(value)); }); } function setExternalLdapConfig(externalLdapConfig, callback) { assert.strictEqual(typeof externalLdapConfig, 'object'); assert.strictEqual(typeof callback, 'function'); getExternalLdapConfig(function (error, curentConfig) { if (error) return callback(error); externalLdap.injectPrivateFields(externalLdapConfig, curentConfig); externalLdap.testConfig(externalLdapConfig, function (error) { if (error) return callback(error); settingsdb.set(exports.EXTERNAL_LDAP_KEY, JSON.stringify(externalLdapConfig), function (error) { if (error) return callback(error); notifyChange(exports.EXTERNAL_LDAP_KEY, externalLdapConfig); callback(null); }); }); }); } function getRegistryConfig(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.REGISTRY_CONFIG_KEY, function (error, value) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.REGISTRY_CONFIG_KEY]); if (error) return callback(error); callback(null, JSON.parse(value)); }); } function setRegistryConfig(registryConfig, callback) { assert.strictEqual(typeof registryConfig, 'object'); assert.strictEqual(typeof callback, 'function'); getRegistryConfig(function (error, curentConfig) { if (error) return callback(error); docker.injectPrivateFields(registryConfig, curentConfig); docker.testRegistryConfig(registryConfig, function (error) { if (error) return callback(error); settingsdb.set(exports.REGISTRY_CONFIG_KEY, JSON.stringify(registryConfig), function (error) { if (error) return callback(error); notifyChange(exports.REGISTRY_CONFIG_KEY, registryConfig); callback(null); }); }); }); } function getLicenseKey(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.LICENSE_KEY, function (error, value) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.LICENSE_KEY]); if (error) return callback(error); callback(null, value); }); } function setLicenseKey(licenseKey, callback) { assert.strictEqual(typeof licenseKey, 'string'); assert.strictEqual(typeof callback, 'function'); settingsdb.set(exports.LICENSE_KEY, licenseKey, function (error) { if (error) return callback(error); notifyChange(exports.LICENSE_KEY, licenseKey); callback(null); }); } function getCloudronId(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.CLOUDRON_ID_KEY, function (error, value) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_ID_KEY]); if (error) return callback(error); callback(null, value); }); } function setCloudronId(cid, callback) { assert.strictEqual(typeof cid, 'string'); assert.strictEqual(typeof callback, 'function'); settingsdb.set(exports.CLOUDRON_ID_KEY, cid, function (error) { if (error) return callback(error); notifyChange(exports.CLOUDRON_ID_KEY, cid); callback(null); }); } function getCloudronToken(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.get(exports.CLOUDRON_TOKEN_KEY, function (error, value) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_TOKEN_KEY]); if (error) return callback(error); callback(null, value); }); } function setCloudronToken(token, callback) { assert.strictEqual(typeof token, 'string'); assert.strictEqual(typeof callback, 'function'); settingsdb.set(exports.CLOUDRON_TOKEN_KEY, token, function (error) { if (error) return callback(error); notifyChange(exports.CLOUDRON_TOKEN_KEY, token); callback(null); }); } function getAll(callback) { assert.strictEqual(typeof callback, 'function'); settingsdb.getAll(function (error, settings) { if (error) return callback(error); var result = _.extend({ }, gDefaults); settings.forEach(function (setting) { result[setting.name] = setting.value; }); // convert booleans result[exports.DYNAMIC_DNS_KEY] = !!result[exports.DYNAMIC_DNS_KEY]; result[exports.UNSTABLE_APPS_KEY] = !!result[exports.UNSTABLE_APPS_KEY]; result[exports.DEMO_KEY] = !!result[exports.DEMO_KEY]; // convert JSON objects [exports.BACKUP_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY, exports.EXTERNAL_LDAP_KEY, exports.REGISTRY_CONFIG_KEY ].forEach(function (key) { result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]); }); callback(null, result); }); } function initCache(callback) { debug('initCache: pre-load settings'); getAll(function (error, allSettings) { if (error) return callback(error); gCache = { apiServerOrigin: allSettings[exports.API_SERVER_ORIGIN_KEY], webServerOrigin: allSettings[exports.WEB_SERVER_ORIGIN_KEY], adminDomain: allSettings[exports.ADMIN_DOMAIN_KEY], adminFqdn: allSettings[exports.ADMIN_FQDN_KEY], isDemo: allSettings[exports.DEMO_KEY] }; callback(); }); } // this is together so we can do this in a transaction later function setAdmin(adminDomain, adminFqdn, callback) { assert.strictEqual(typeof adminDomain, 'string'); assert.strictEqual(typeof adminFqdn, 'string'); assert.strictEqual(typeof callback, 'function'); settingsdb.set(exports.ADMIN_DOMAIN_KEY, adminDomain, function (error) { if (error) return callback(error); settingsdb.set(exports.ADMIN_FQDN_KEY, adminFqdn, function (error) { if (error) return callback(error); gCache.adminDomain = adminDomain; gCache.adminFqdn = adminFqdn; callback(null); }); }); } function setApiServerOrigin(origin, callback) { assert.strictEqual(typeof origin, 'string'); assert.strictEqual(typeof callback, 'function'); settingsdb.set(exports.API_SERVER_ORIGIN_KEY, origin, function (error) { if (error) return callback(error); gCache.apiServerOrigin = origin; notifyChange(exports.API_SERVER_ORIGIN_KEY, origin); callback(null); }); } function apiServerOrigin() { return gCache.apiServerOrigin; } function webServerOrigin() { return gCache.webServerOrigin; } function adminDomain() { return gCache.adminDomain; } function adminFqdn() { return gCache.adminFqdn; } function isDemo() { return gCache.isDemo; } function mailFqdn() { return adminFqdn(); } function adminOrigin() { return 'https://' + adminFqdn(); }