'use strict'; exports = module.exports = { setup, restore, activate, getStatus, }; const appstore = require('./appstore.js'), assert = require('node:assert'), backupSites = require('./backupsites.js'), backups = require('./backups.js'), backuptask = require('./backuptask.js'), BoxError = require('./boxerror.js'), dashboard = require('./dashboard.js'), constants = require('./constants.js'), debug = require('debug')('box:provision'), dns = require('./dns.js'), domains = require('./domains.js'), eventlog = require('./eventlog.js'), fs = require('node:fs'), mail = require('./mail.js'), mailServer = require('./mailserver.js'), network = require('./network.js'), oidcClients = require('./oidcclients.js'), openssl = require('./openssl.js'), platform = require('./platform.js'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), semver = require('semver'), paths = require('./paths.js'), system = require('./system.js'), tasks = require('./tasks.js'), users = require('./users.js'), tld = require('tldjs'), tokens = require('./tokens.js'); // we cannot use tasks since the tasks table gets overwritten when db is imported const gStatus = { setup: { active: false, message: '', errorMessage: null, startTime: null }, restore: { active: false, message: '', errorMessage: null, startTime: null }, activated: false, adminFqdn: null, provider: null, version: constants.VERSION // for reloading dashboard }; function setProgress(task, message) { debug(`setProgress: ${task} - ${message}`); gStatus[task].message = message; } async function ensureDhparams() { if (fs.existsSync(paths.DHPARAMS_FILE)) return; debug('ensureDhparams: generating dhparams'); const dhparams = await openssl.generateDhparam(); 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 dashboard.clearLocation(); await mail.clearDomains(); await domains.clear(); } async function setupTask(domain, auditSource) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof auditSource, 'object'); const location = { subdomain: constants.DASHBOARD_SUBDOMAIN, domain }; try { debug(`setupTask: subdomain ${location.subdomain} and domain ${location.domain}`); await dns.registerLocations([location], { overwriteDns: true }, (progress) => setProgress('setup', progress.message)); await dns.waitForLocations([location], (progress) => setProgress('setup', progress.message)); await reverseProxy.ensureCertificate(location, {}, auditSource); await ensureDhparams(); await dashboard.setupLocation(constants.DASHBOARD_SUBDOMAIN, domain, auditSource); await backupSites.addDefault(auditSource); setProgress('setup', 'Done'), await eventlog.add(eventlog.ACTION_PROVISION, auditSource, {}); } catch (error) { debug('setupTask: error. %o', error); gStatus.setup.errorMessage = error.message; } gStatus.setup.active = false; } async function setup(domainConfig, ipv4Config, ipv6Config, auditSource) { assert.strictEqual(typeof domainConfig, 'object'); assert.strictEqual(typeof ipv4Config, 'object'); assert.strictEqual(typeof ipv6Config, 'object'); assert.strictEqual(typeof auditSource, 'object'); if (gStatus.setup.active || gStatus.restore.active) throw new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring'); gStatus.setup = { active: true, errorMessage: '', message: 'Adding domain', startTime: (new Date()).toISOString() }; try { const activated = await users.isActivated(); if (activated) throw new BoxError(BoxError.CONFLICT, 'Already activated'); await unprovision(); const domain = domainConfig.domain.toLowerCase(); const zoneName = domainConfig.zoneName ? domainConfig.zoneName : (tld.getDomain(domain) || domain); debug(`setup: 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 mailServer.setLocation(constants.DASHBOARD_SUBDOMAIN, domain); // default mail location. do this before we add the domain for upserting mail DNS await domains.add(domain, data, auditSource); await network.setIPv4Config(ipv4Config); await network.setIPv6Config(ipv6Config); safe(setupTask(domain, auditSource), { debug }); // now that args are validated run the task in the background } catch (error) { debug('setup: error. %o', error); gStatus.setup.active = false; gStatus.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}`); await appstore.registerCloudron3(); 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: oidcClients.ID_WEBADMIN, identifier: ownerId, allowedIpRanges: '', expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }; const result = await tokens.add(token); await eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, {}); safe(platform.onActivated({ skipDnsSetup: false }), { debug }); // background return { userId: ownerId, token: result.accessToken, expires: result.expires }; } async function restoreTask(backupSite, remotePath, ipv4Config, ipv6Config, options, auditSource) { assert.strictEqual(typeof backupSite, 'object'); assert.strictEqual(typeof remotePath, 'string'); assert.strictEqual(typeof ipv4Config, 'object'); assert.strictEqual(typeof ipv6Config, 'object'); assert.strictEqual(typeof options, 'object'); // { skipDnsSetup } assert.strictEqual(typeof auditSource, 'object'); try { setProgress('restore', 'Preparing backup site'); await backupSites.storageApi(backupSite).setup(backupSite.config); setProgress('restore', 'Downloading box backup'); await backuptask.restore(backupSite, 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 mailRemotePath = mailBackups[0].remotePath; await backuptask.downloadMail(backupSite, mailRemotePath, (progress) => setProgress('restore', progress.message)); await ensureDhparams(); await network.setIPv4Config(ipv4Config); await network.setIPv6Config(ipv6Config); await reverseProxy.restoreFallbackCertificates(); await backupSites.reinitAll(); // when creating a sql dump during a full backup, the task has not completed yet. we mark it as completed here for the Sites UI to not indicate a crash // siteId can be missing when restoring manually instead of via the backup config const backupTasks = await tasks.list(1, 1, { type: options.siteId ? tasks.TASK_FULL_BACKUP_PREFIX + options.siteId : null, prefix: !options.siteId ? tasks.TASK_FULL_BACKUP_PREFIX : null // guess that the latest full backup was the site that was used }); await tasks.setCompleted(backupTasks[0].id, { error: null }); const location = await dashboard.getLocation(); // load this fresh from after the backup.restore if (!options.skipDnsSetup) { await dns.registerLocations([location], { overwriteDns: true }, (progress) => setProgress('restore', progress.message)); await dns.waitForLocations([location], (progress) => setProgress('setup', progress.message)); await reverseProxy.ensureCertificate(location, {}, auditSource); } await dashboard.setupLocation(location.subdomain, location.domain, auditSource); await eventlog.add(eventlog.ACTION_RESTORE, auditSource, { remotePath }); await appstore.checkSubscription(); // never throws. worst case, user has to visit the Account view to refresh subscription info safe(platform.onActivated({ skipDnsSetup: options.skipDnsSetup }), { debug }); // background await backupSites.storageApi(backupSite).teardown(backupSite.config); } catch (error) { debug('restoreTask: error. %o', error); gStatus.restore.errorMessage = error ? error.message : ''; } gStatus.restore.active = false; } async function restore(backupConfig, remotePath, version, ipv4Config, ipv6Config, options, auditSource) { assert.strictEqual(typeof backupConfig, 'object'); // { format, config, provider, encryptionPassword } assert.strictEqual(typeof remotePath, 'string'); assert.strictEqual(typeof version, 'string'); assert.strictEqual(typeof ipv4Config, 'object'); assert.strictEqual(typeof ipv6Config, 'object'); assert.strictEqual(typeof options, 'object'); // { skipDnsSetup, siteId } 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 (gStatus.setup.active || gStatus.restore.active) throw new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring'); gStatus.restore = { active: true, errorMessage: '', message: 'Testing backup config', startTime: (new Date()).toISOString() }; try { const activated = await users.isActivated(); if (activated) throw new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.'); const backupSite = await backupSites.createPseudo({ id: `cloudron-restore`, provider: backupConfig.provider, config: backupConfig.config, format: backupConfig.format, encryptionPassword: backupConfig.encryptionPassword ?? null, encryptedFilenames: !!backupConfig.encryptedFilenames }); const ipv4Error = await network.testIPv4Config(ipv4Config); if (ipv4Error) throw ipv4Error; const ipv6Error = await network.testIPv6Config(ipv6Config); if (ipv6Error) throw ipv6Error; safe(restoreTask(backupSite, remotePath, ipv4Config, ipv6Config, options, auditSource), { debug }); // now that args are validated run the task in the background } catch (error) { debug('restore: error. %o', error); gStatus.restore.active = false; gStatus.restore.errorMessage = error.message; throw error; } } async function getStatus() { const { fqdn:dashboardFqdn } = await dashboard.getLocation(); gStatus.adminFqdn = dashboardFqdn; // indicator for main.js if dnssetup was already done but not activated gStatus.activated = await users.isActivated(); // indicator for admin setup gStatus.provider = await system.getProvider(); // preselect dns provider and ami token return gStatus; }