Files
cloudron-box/src/provision.js

284 lines
12 KiB
JavaScript
Raw Normal View History

2018-01-29 15:47:26 -08:00
'use strict';
exports = module.exports = {
2021-05-01 11:21:09 -07:00
setup,
restore,
activate,
2023-08-04 11:01:45 +05:30
getStatus,
2018-01-29 15:47:26 -08:00
};
const appstore = require('./appstore.js'),
assert = require('node:assert'),
2025-09-12 09:48:37 +02:00
backupSites = require('./backupsites.js'),
backups = require('./backups.js'),
2021-07-14 11:07:19 -07:00
backuptask = require('./backuptask.js'),
2019-10-22 20:36:20 -07:00
BoxError = require('./boxerror.js'),
dashboard = require('./dashboard.js'),
2018-01-29 15:47:26 -08:00
constants = require('./constants.js'),
2018-12-11 15:29:47 -08:00
debug = require('debug')('box:provision'),
2023-08-14 09:40:31 +05:30
dns = require('./dns.js'),
2018-01-29 15:47:26 -08:00
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
fs = require('node:fs'),
2018-01-29 15:47:26 -08:00
mail = require('./mail.js'),
mailServer = require('./mailserver.js'),
network = require('./network.js'),
2025-06-11 22:53:29 +02:00
oidcClients = require('./oidcclients.js'),
openssl = require('./openssl.js'),
2023-08-12 19:28:07 +05:30
platform = require('./platform.js'),
2021-05-04 21:40:11 -07:00
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
2018-01-29 15:47:26 -08:00
semver = require('semver'),
paths = require('./paths.js'),
system = require('./system.js'),
tasks = require('./tasks.js'),
2018-04-29 10:58:45 -07:00
users = require('./users.js'),
2018-01-29 15:47:26 -08:00
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
};
2018-01-29 15:47:26 -08:00
2022-04-15 17:29:15 -05:00
function setProgress(task, message) {
2018-12-22 20:48:03 -08:00
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');
2023-08-14 09:40:31 +05:30
const location = { subdomain: constants.DASHBOARD_SUBDOMAIN, domain };
try {
2024-10-30 20:58:31 +01:00
debug(`setupTask: subdomain ${location.subdomain} and domain ${location.domain}`);
2023-08-14 09:40:31 +05:30
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);
2025-09-12 09:48:37 +02:00
await backupSites.addDefault(auditSource);
setProgress('setup', 'Done'),
await eventlog.add(eventlog.ACTION_PROVISION, auditSource, {});
} catch (error) {
2023-08-14 09:40:31 +05:30
debug('setupTask: error. %o', error);
gStatus.setup.errorMessage = error.message;
}
gStatus.setup.active = false;
}
2024-04-25 19:33:04 +02:00
async function setup(domainConfig, ipv4Config, ipv6Config, auditSource) {
assert.strictEqual(typeof domainConfig, 'object');
assert.strictEqual(typeof ipv4Config, 'object');
2024-04-25 19:33:04 +02:00
assert.strictEqual(typeof ipv6Config, 'object');
assert.strictEqual(typeof auditSource, 'object');
2018-01-29 15:47:26 -08:00
if (gStatus.setup.active || gStatus.restore.active) throw new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring');
2018-01-29 15:47:26 -08:00
gStatus.setup = { active: true, errorMessage: '', message: 'Adding domain', startTime: (new Date()).toISOString() };
2018-01-29 15:47:26 -08:00
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);
2018-11-10 00:43:46 -08:00
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'
};
2018-11-10 00:43:46 -08:00
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);
2024-04-25 19:33:04 +02:00
await network.setIPv6Config(ipv6Config);
2021-09-26 21:24:31 -07:00
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;
}
2018-01-29 15:47:26 -08:00
}
2021-07-15 09:50:11 -07:00
async function activate(username, password, email, displayName, ip, auditSource) {
2018-01-29 15:47:26 -08:00
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');
2021-07-15 09:50:11 -07:00
debug(`activate: user: ${username} email:${email}`);
2018-01-29 15:47:26 -08:00
2025-09-24 21:25:31 +02:00
await appstore.registerCloudron3();
2021-07-19 12:43:30 -07:00
const [error, ownerId] = await safe(users.createOwner(email, username, password, displayName, auditSource));
2021-07-15 09:50:11 -07:00
if (error && error.reason === BoxError.ALREADY_EXISTS) throw new BoxError(BoxError.CONFLICT, 'Already activated');
if (error) throw error;
2018-01-29 15:47:26 -08:00
2025-06-11 22:53:29 +02:00
const token = { clientId: oidcClients.ID_WEBADMIN, identifier: ownerId, allowedIpRanges: '', expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS };
2021-07-15 09:50:11 -07:00
const result = await tokens.add(token);
2018-01-29 15:47:26 -08:00
2022-02-24 20:04:46 -08:00
await eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, {});
2018-01-29 15:47:26 -08:00
2025-12-05 16:12:59 +01:00
safe(platform.onActivated({ skipDnsSetup: false }), { debug }); // background
2021-06-04 09:28:40 -07:00
2021-07-15 09:50:11 -07:00
return {
2021-07-19 12:43:30 -07:00
userId: ownerId,
2021-07-15 09:50:11 -07:00
token: result.accessToken,
expires: result.expires
};
2018-01-29 15:47:26 -08:00
}
2025-09-12 09:48:37 +02:00
async function restoreTask(backupSite, remotePath, ipv4Config, ipv6Config, options, auditSource) {
assert.strictEqual(typeof backupSite, 'object');
assert.strictEqual(typeof remotePath, 'string');
assert.strictEqual(typeof ipv4Config, 'object');
2024-04-25 19:33:04 +02:00
assert.strictEqual(typeof ipv6Config, 'object');
2025-08-05 14:13:39 +02:00
assert.strictEqual(typeof options, 'object'); // { skipDnsSetup }
2018-11-10 00:43:46 -08:00
assert.strictEqual(typeof auditSource, 'object');
2018-01-29 15:47:26 -08:00
try {
2025-09-12 09:48:37 +02:00
setProgress('restore', 'Preparing backup site');
await backupSites.storageApi(backupSite).setup(backupSite.config);
setProgress('restore', 'Downloading box backup');
2025-09-12 09:48:37 +02:00
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;
2025-09-12 09:48:37 +02:00
await backuptask.downloadMail(backupSite, mailRemotePath, (progress) => setProgress('restore', progress.message));
await ensureDhparams();
await network.setIPv4Config(ipv4Config);
2024-04-25 19:33:04 +02:00
await network.setIPv6Config(ipv6Config);
await reverseProxy.restoreFallbackCertificates();
2018-01-29 15:47:26 -08:00
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 });
2023-08-14 09:40:31 +05:30
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 });
2025-10-02 11:37:26 +02:00
await appstore.checkSubscription(); // never throws. worst case, user has to visit the Account view to refresh subscription info
2025-12-05 16:12:59 +01:00
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;
}
2024-04-27 12:46:37 +02:00
async function restore(backupConfig, remotePath, version, ipv4Config, ipv6Config, options, auditSource) {
2025-08-05 14:13:39 +02:00
assert.strictEqual(typeof backupConfig, 'object'); // { format, config, provider, encryptionPassword }
2022-04-13 21:23:12 -05:00
assert.strictEqual(typeof remotePath, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof ipv4Config, 'object');
2024-04-27 12:46:37 +02:00
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.');
2018-01-29 15:47:26 -08:00
2025-09-12 09:48:37 +02:00
const backupSite = await backupSites.createPseudo({
id: `cloudron-restore`,
provider: backupConfig.provider,
config: backupConfig.config,
format: backupConfig.format,
2025-08-05 14:13:39 +02:00
encryptionPassword: backupConfig.encryptionPassword ?? null,
encryptedFilenames: !!backupConfig.encryptedFilenames
});
2025-12-02 15:19:59 +01:00
const ipv4Error = await network.testIPv4Config(ipv4Config);
if (ipv4Error) throw ipv4Error;
const ipv6Error = await network.testIPv6Config(ipv6Config);
if (ipv6Error) throw ipv6Error;
2025-09-12 09:48:37 +02:00
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;
}
2018-01-29 15:47:26 -08:00
}
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;
}