260 lines
11 KiB
JavaScript
260 lines
11 KiB
JavaScript
'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);
|
|
}
|