'use strict'; exports = module.exports = { setup, restore, activate, getStatus }; const assert = require('assert'), backups = require('./backups.js'), backuptask = require('./backuptask.js'), BoxError = require('./boxerror.js'), branding = require('./branding.js'), constants = require('./constants.js'), cloudron = require('./cloudron.js'), debug = require('debug')('box:provision'), domains = require('./domains.js'), eventlog = require('./eventlog.js'), fs = require('fs'), mail = require('./mail.js'), mounts = require('./mounts.js'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), semver = require('semver'), settings = require('./settings.js'), sysinfo = require('./sysinfo.js'), paths = require('./paths.js'), users = require('./users.js'), tld = require('tldjs'), tokens = require('./tokens.js'), _ = require('underscore'); // we cannot use tasks since the tasks table gets overwritten when db is imported const gProvisionStatus = { setup: { active: false, message: '', errorMessage: null }, restore: { active: false, message: '', errorMessage: null } }; function setProgress(task, message) { debug(`setProgress: ${task} - ${message}`); gProvisionStatus[task].message = message; } async function ensureDhparams() { if (fs.existsSync(paths.DHPARAMS_FILE)) return; debug('ensureDhparams: generating dhparams'); const dhparams = safe.child_process.execSync('openssl dhparam -dsaparam 2048'); if (!dhparams) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error); if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`); } async function unprovision() { // TODO: also cancel any existing configureWebadmin task await settings.setDashboardLocation('', ''); await mail.clearDomains(); await domains.clear(); } async function setupTask(domain, auditSource) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof auditSource, 'object'); try { await cloudron.setupDnsAndCert(constants.DASHBOARD_LOCATION, domain, auditSource, (progress) => setProgress('setup', progress.message)); await ensureDhparams(); await cloudron.setDashboardDomain(domain, auditSource); setProgress('setup', 'Done'), await eventlog.add(eventlog.ACTION_PROVISION, auditSource, {}); } catch (error) { gProvisionStatus.setup.errorMessage = error ? error.message : ''; } gProvisionStatus.setup.active = false; } async function setup(domainConfig, sysinfoConfig, auditSource) { assert.strictEqual(typeof domainConfig, 'object'); assert.strictEqual(typeof sysinfoConfig, 'object'); assert.strictEqual(typeof auditSource, 'object'); if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) throw new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring'); gProvisionStatus.setup = { active: true, errorMessage: '', message: 'Adding domain' }; try { const activated = await users.isActivated(); if (activated) throw new BoxError(BoxError.CONFLICT, 'Already activated', { activate: true }); await unprovision(); const domain = domainConfig.domain.toLowerCase(); const zoneName = domainConfig.zoneName ? domainConfig.zoneName : (tld.getDomain(domain) || domain); debug(`setup: Setting up Cloudron with domain ${domain} and zone ${zoneName}`); const data = { zoneName: zoneName, provider: domainConfig.provider, config: domainConfig.config, fallbackCertificate: domainConfig.fallbackCertificate || null, tlsConfig: domainConfig.tlsConfig || { provider: 'letsencrypt-prod' }, dkimSelector: 'cloudron' }; await settings.setMailLocation(domain, `${constants.DASHBOARD_LOCATION}.${domain}`); // default mail location. do this before we add the domain for upserting mail DNS await domains.add(domain, data, auditSource); await settings.setSysinfoConfig(sysinfoConfig); safe(setupTask(domain, auditSource), { debug }); // now that args are validated run the task in the background } catch (error) { debug('setup: error', error); gProvisionStatus.setup.active = false; gProvisionStatus.setup.errorMessage = error.message; throw error; } } async function activate(username, password, email, displayName, ip, auditSource) { assert.strictEqual(typeof username, 'string'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof displayName, 'string'); assert.strictEqual(typeof ip, 'string'); assert.strictEqual(typeof auditSource, 'object'); debug(`activate: user: ${username} email:${email}`); const [error, ownerId] = await safe(users.createOwner(email, username, password, displayName, auditSource)); if (error && error.reason === BoxError.ALREADY_EXISTS) throw new BoxError(BoxError.CONFLICT, 'Already activated'); if (error) throw error; const token = { clientId: tokens.ID_WEBADMIN, identifier: ownerId, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }; const result = await tokens.add(token); await eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, {}); setImmediate(() => safe(cloudron.onActivated({}), { debug })); return { userId: ownerId, token: result.accessToken, expires: result.expires }; } async function restoreTask(backupConfig, remotePath, sysinfoConfig, options, auditSource) { assert.strictEqual(typeof backupConfig, 'object'); assert.strictEqual(typeof remotePath, 'string'); assert.strictEqual(typeof sysinfoConfig, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof auditSource, 'object'); try { setProgress('restore', 'Downloading box backup'); await backuptask.restore(backupConfig, remotePath, (progress) => setProgress('restore', progress.message)); setProgress('restore', 'Downloading mail backup'); const mailBackups = await backups.getByIdentifierAndStatePaged(backups.BACKUP_IDENTIFIER_MAIL, backups.BACKUP_STATE_NORMAL, 1, 1); if (mailBackups.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'mail backup not found'); const mailRestoreConfig = { backupConfig, remotePath: mailBackups[0].remotePath, backupFormat: mailBackups[0].format }; await backuptask.downloadMail(mailRestoreConfig, (progress) => setProgress('restore', progress.message)); await ensureDhparams(); await settings.setSysinfoConfig(sysinfoConfig); await reverseProxy.restoreFallbackCertificates(); const dashboardDomain = settings.dashboardDomain(); // load this fresh from after the backup.restore if (!options.skipDnsSetup) await cloudron.setupDnsAndCert(constants.DASHBOARD_LOCATION, dashboardDomain, auditSource, (progress) => setProgress('restore', progress.message)); await cloudron.setDashboardDomain(dashboardDomain, auditSource); await settings.setBackupCredentials(backupConfig); // update just the credentials and not the policy and flags await eventlog.add(eventlog.ACTION_RESTORE, auditSource, { remotePath }); setImmediate(() => safe(cloudron.onActivated(options), { debug })); } catch (error) { gProvisionStatus.restore.errorMessage = error ? error.message : ''; } gProvisionStatus.restore.active = false; } async function restore(backupConfig, remotePath, version, sysinfoConfig, options, auditSource) { assert.strictEqual(typeof backupConfig, 'object'); assert.strictEqual(typeof remotePath, 'string'); assert.strictEqual(typeof version, 'string'); assert.strictEqual(typeof sysinfoConfig, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof auditSource, 'object'); if (!semver.valid(version)) throw new BoxError(BoxError.BAD_FIELD, 'version is not a valid semver'); if (constants.VERSION !== version) throw new BoxError(BoxError.BAD_STATE, `Run "cloudron-setup --version ${version}" on a fresh Ubuntu installation to restore from this backup`); if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) throw new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring'); gProvisionStatus.restore = { active: true, errorMessage: '', message: 'Testing backup config' }; try { const activated = await users.isActivated(); if (activated) throw new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.'); if (mounts.isManagedProvider(backupConfig.provider)) { const error = mounts.validateMountOptions(backupConfig.provider, backupConfig.mountOptions); if (error) throw error; const newMount = { name: 'backup', hostPath: paths.MANAGED_BACKUP_MOUNT_DIR, mountType: backupConfig.provider, mountOptions: backupConfig.mountOptions }; await safe(mounts.tryAddMount(newMount, { timeout: 10 })); // 10 seconds } let error = await backups.testProviderConfig(backupConfig); if (error) throw error; if ('password' in backupConfig) { backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password); delete backupConfig.password; } else { backupConfig.encryption = null; } error = await sysinfo.testIPv4Config(sysinfoConfig); if (error) throw error; safe(restoreTask(backupConfig, remotePath, sysinfoConfig, options, auditSource), { debug }); // now that args are validated run the task in the background } catch (error) { gProvisionStatus.restore.active = false; gProvisionStatus.restore.errorMessage = error ? error.message : ''; throw error; } } async function getStatus() { const activated = await users.isActivated(); const allSettings = await settings.list(); return _.extend({ version: constants.VERSION, apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool webServerOrigin: settings.webServerOrigin(), // used by CaaS tool cloudronName: allSettings[settings.CLOUDRON_NAME_KEY], footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER), adminFqdn: settings.dashboardDomain() ? settings.dashboardFqdn() : null, language: allSettings[settings.LANGUAGE_KEY], activated: activated, provider: settings.provider() // used by setup wizard of marketplace images }, gProvisionStatus); }