12e073e8cf
mostly because code is being autogenerated by all the AI stuff using this prefix. it's also used in the stack trace.
267 lines
11 KiB
JavaScript
267 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
setup,
|
|
restore,
|
|
activate,
|
|
getStatus,
|
|
};
|
|
|
|
const appstore = require('./appstore.js'),
|
|
assert = require('node:assert'),
|
|
backupTargets = require('./backuptargets.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'),
|
|
platform = require('./platform.js'),
|
|
reverseProxy = require('./reverseproxy.js'),
|
|
safe = require('safetydance'),
|
|
shell = require('./shell.js')('provision'),
|
|
semver = require('semver'),
|
|
paths = require('./paths.js'),
|
|
system = require('./system.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 shell.spawn('openssl', ['dhparam', '-dsaparam', '2048'], { encoding: 'utf8' });
|
|
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 backupTargets.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}`);
|
|
|
|
const dashboardLocation = await dashboard.getLocation();
|
|
await appstore.registerCloudron3(dashboardLocation.domain, constants.VERSION);
|
|
|
|
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, {});
|
|
|
|
setImmediate(() => safe(platform.onActivated({ skipDnsSetup: false }), { debug }));
|
|
|
|
return {
|
|
userId: ownerId,
|
|
token: result.accessToken,
|
|
expires: result.expires
|
|
};
|
|
}
|
|
|
|
async function restoreTask(backupTarget, remotePath, ipv4Config, ipv6Config, options, auditSource) {
|
|
assert.strictEqual(typeof backupTarget, '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 target');
|
|
await backupTargets.storageApi(backupTarget).setup(backupTarget.config);
|
|
|
|
setProgress('restore', 'Downloading box backup');
|
|
await backuptask.restore(backupTarget, 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(backupTarget, mailRemotePath, (progress) => setProgress('restore', progress.message));
|
|
|
|
await ensureDhparams();
|
|
await network.setIPv4Config(ipv4Config);
|
|
await network.setIPv6Config(ipv6Config);
|
|
await reverseProxy.restoreFallbackCertificates();
|
|
|
|
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 });
|
|
|
|
setImmediate(() => safe(platform.onActivated({ backupTarget, skipDnsSetup: options.skipDnsSetup }), { debug }));
|
|
} 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 }
|
|
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 backupTarget = await backupTargets.createPseudo({
|
|
id: `cloudron-restore`,
|
|
provider: backupConfig.provider,
|
|
config: backupConfig.config,
|
|
format: backupConfig.format,
|
|
encryptionPassword: backupConfig.encryptionPassword ?? null,
|
|
encryptedFilenames: !!backupConfig.encryptedFilenames
|
|
});
|
|
|
|
const error = await network.testIPv4Config(ipv4Config);
|
|
if (error) throw error;
|
|
|
|
safe(restoreTask(backupTarget, 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;
|
|
}
|