Files
cloudron-box/src/setup.js

339 lines
14 KiB
JavaScript
Raw Normal View History

2018-01-29 15:47:26 -08:00
'use strict';
exports = module.exports = {
provision: provision,
2018-01-29 15:47:26 -08:00
restore: restore,
getStatus: getStatus,
activate: activate,
configureWebadmin: configureWebadmin,
2018-01-29 15:47:26 -08:00
SetupError: SetupError
};
2018-07-25 10:34:57 -07:00
var assert = require('assert'),
2018-01-29 15:47:26 -08:00
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
config = require('./config.js'),
constants = require('./constants.js'),
clients = require('./clients.js'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:setup'),
domains = require('./domains.js'),
2018-04-29 11:20:12 -07:00
DomainsError = domains.DomainsError,
2018-01-29 15:47:26 -08:00
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
2018-01-29 15:47:26 -08:00
safe = require('safetydance'),
semver = require('semver'),
settingsdb = require('./settingsdb.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
2018-04-29 10:58:45 -07:00
users = require('./users.js'),
2018-04-29 17:37:53 -07:00
UsersError = users.UsersError,
2018-01-29 15:47:26 -08:00
tld = require('tldjs'),
util = require('util');
var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var gWebadminStatus = {
dns: false,
tls: false,
configuring: false,
restore: {
active: false,
error: null
}
2018-01-29 15:47:26 -08:00
};
function SetupError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(SetupError, Error);
SetupError.BAD_FIELD = 'Field error';
SetupError.BAD_STATE = 'Bad State';
2018-01-29 15:47:26 -08:00
SetupError.ALREADY_SETUP = 'Already Setup';
SetupError.INTERNAL_ERROR = 'Internal Error';
SetupError.EXTERNAL_ERROR = 'External Error';
SetupError.ALREADY_PROVISIONED = 'Already Provisioned';
function autoprovision(autoconf, callback) {
assert.strictEqual(typeof autoconf, 'object');
2018-01-29 15:47:26 -08:00
assert.strictEqual(typeof callback, 'function');
async.eachSeries(Object.keys(autoconf), function (key, iteratorDone) {
2018-11-02 17:10:16 -07:00
debug(`autoprovision: ${key}`);
2018-01-29 15:47:26 -08:00
switch (key) {
case 'appstoreConfig':
if (config.provider() === 'caas') { // skip registration
settingsdb.set(settings.APPSTORE_CONFIG_KEY, JSON.stringify(autoconf[key]), iteratorDone);
} else { // register cloudron
settings.setAppstoreConfig(autoconf[key], iteratorDone);
}
break;
case 'caasConfig':
settingsdb.set(settings.CAAS_CONFIG_KEY, JSON.stringify(autoconf[key]), iteratorDone);
break;
case 'backupConfig':
settings.setBackupConfig(autoconf[key], iteratorDone);
break;
2018-01-29 15:47:26 -08:00
default:
debug(`autoprovision: ${key} ignored`);
return iteratorDone();
}
}, function (error) {
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
callback(null);
});
2018-01-29 15:47:26 -08:00
}
function configureWebadmin(callback) {
callback = callback || NOOP_CALLBACK;
debug('configureWebadmin: adminDomain:%s status:%j', config.adminDomain(), gWebadminStatus);
if (process.env.BOX_ENV === 'test' || !config.adminDomain() || gWebadminStatus.configuring) return callback();
gWebadminStatus.configuring = true; // re-entracy guard
function configureReverseProxy(error) {
2018-01-30 19:59:09 -08:00
debug('configureReverseProxy: error %j', error || null);
2018-01-29 15:47:26 -08:00
2018-01-30 15:16:34 -08:00
reverseProxy.configureAdmin({ userId: null, username: 'setup' }, function (error) {
2018-03-08 18:26:44 -08:00
debug('configureWebadmin: done error: %j', error || {});
gWebadminStatus.configuring = false;
2018-01-29 15:47:26 -08:00
2018-03-08 18:26:44 -08:00
if (error) return callback(error);
2018-01-29 15:47:26 -08:00
2018-03-08 18:26:44 -08:00
gWebadminStatus.tls = true;
2018-01-29 15:47:26 -08:00
2018-03-08 18:26:44 -08:00
callback();
2018-01-29 15:47:26 -08:00
});
}
// update the DNS. configure nginx regardless of whether it succeeded so that
// box is accessible even if dns creds are invalid
sysinfo.getPublicIp(function (error, ip) {
if (error) return configureReverseProxy(error);
2018-01-29 15:47:26 -08:00
2018-03-08 18:26:44 -08:00
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
debug('addWebadminDnsRecord: updated records with error:', error);
if (error) return configureReverseProxy(error);
2018-01-29 15:47:26 -08:00
domains.waitForDnsRecord(config.adminLocation(), config.adminDomain(), 'A', ip, { interval: 30000, times: 50000 }, function (error) {
if (error) return configureReverseProxy(error);
2018-01-29 15:47:26 -08:00
gWebadminStatus.dns = true;
configureReverseProxy();
2018-01-29 15:47:26 -08:00
});
});
});
}
2018-11-10 00:43:46 -08:00
function provision(dnsConfig, autoconf, auditSource, callback) {
2018-01-29 15:47:26 -08:00
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof autoconf, 'object');
2018-11-10 00:43:46 -08:00
assert.strictEqual(typeof auditSource, 'object');
2018-01-29 15:47:26 -08:00
assert.strictEqual(typeof callback, 'function');
if (config.adminDomain()) return callback(new SetupError(SetupError.ALREADY_SETUP));
if (gWebadminStatus.configuring || gWebadminStatus.restore.active) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
const domain = dnsConfig.domain.toLowerCase();
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
const adminFqdn = 'my' + (dnsConfig.config.hyphenatedSubdomains ? '-' : '.') + domain;
2018-01-29 15:47:26 -08:00
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
2018-01-29 15:47:26 -08:00
domains.get(domain, function (error, result) {
2018-06-05 21:13:38 -07:00
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
2018-01-29 15:47:26 -08:00
2018-06-05 21:13:38 -07:00
if (result) return callback(new SetupError(SetupError.BAD_STATE, 'Domain already exists'));
2018-11-10 00:43:46 -08:00
let data = {
zoneName: zoneName,
provider: dnsConfig.provider,
config: dnsConfig.config,
fallbackCertificate: dnsConfig.fallbackCertificate || null,
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
};
async.series([
2018-11-10 00:43:46 -08:00
domains.add.bind(null, domain, data, auditSource),
2018-04-03 14:37:52 -07:00
mail.addDomain.bind(null, domain)
], function (error) {
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
config.setAdminDomain(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setAdminFqdn(adminFqdn);
config.setAdminLocation('my');
2018-11-10 00:43:46 -08:00
eventlog.add(eventlog.ACTION_PROVISION, auditSource, { });
2018-11-02 17:24:38 -07:00
clients.addDefaultClients(config.adminOrigin(), callback);
2018-11-02 17:24:38 -07:00
async.series([
autoprovision.bind(null, autoconf),
configureWebadmin
], NOOP_CALLBACK);
});
2018-01-29 15:47:26 -08:00
});
}
function setTimeZone(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
debug('setTimeZone ip:%s', ip);
superagent.get('https://geolocation.cloudron.io/json').query({ ip: ip }).timeout(10 * 1000).end(function (error, result) {
if ((error && !error.response) || result.statusCode !== 200) {
debug('Failed to get geo location: %s', error.message);
return callback(null);
}
var timezone = safe.query(result.body, 'location.time_zone');
if (!timezone || typeof timezone !== 'string') {
debug('No timezone in geoip response : %j', result.body);
return callback(null);
}
debug('Setting timezone to ', timezone);
settings.setTimeZone(timezone, callback);
});
}
function activate(username, password, email, displayName, ip, auditSource, callback) {
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');
assert.strictEqual(typeof callback, 'function');
debug('activating user:%s email:%s', username, email);
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
2018-04-29 10:58:45 -07:00
users.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
2018-04-29 17:37:53 -07:00
if (error && error.reason === UsersError.ALREADY_EXISTS) return callback(new SetupError(SetupError.ALREADY_PROVISIONED));
if (error && error.reason === UsersError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
2018-01-29 15:47:26 -08:00
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
2018-08-27 14:50:41 -07:00
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
2018-01-29 15:47:26 -08:00
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
2018-01-29 15:47:26 -08:00
callback(null, {
userId: userObject.id,
token: result.accessToken,
expires: result.expires
});
2018-01-29 15:47:26 -08:00
setTimeout(cloudron.onActivated, 3000); // hack for now to not block the above http response
2018-01-29 15:47:26 -08:00
});
});
}
2018-11-10 00:43:46 -08:00
function restore(backupConfig, backupId, version, autoconf, auditSource, callback) {
2018-01-29 15:47:26 -08:00
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof autoconf, 'object');
2018-11-10 00:43:46 -08:00
assert.strictEqual(typeof auditSource, 'object');
2018-01-29 15:47:26 -08:00
assert.strictEqual(typeof callback, 'function');
if (!semver.valid(version)) return callback(new SetupError(SetupError.BAD_STATE, 'version is not a valid semver'));
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new SetupError(SetupError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
if (gWebadminStatus.configuring || gWebadminStatus.restore.active) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
users.isActivated(function (error, activated) {
2018-01-29 15:47:26 -08:00
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
if (activated) return callback(new SetupError(SetupError.ALREADY_PROVISIONED, 'Already activated'));
2018-01-29 15:47:26 -08:00
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new SetupError(SetupError.EXTERNAL_ERROR, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
2018-03-07 13:59:17 -08:00
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider} with format ${backupConfig.format}`);
2018-01-29 15:47:26 -08:00
gWebadminStatus.restore.active = true;
gWebadminStatus.restore.error = null;
2018-01-29 15:47:26 -08:00
callback(null); // do no block
async.series([
backups.restore.bind(null, backupConfig, backupId),
2018-11-10 01:28:59 -08:00
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
autoprovision.bind(null, autoconf),
2018-09-26 12:39:33 -07:00
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
// Once we have a 100% IP based restore, we can skip this
mail.setDnsRecords.bind(null, config.adminDomain()),
2018-01-29 15:47:26 -08:00
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
], function (error) {
debug('restore:', error);
if (error) gWebadminStatus.restore.error = error.message;
gWebadminStatus.restore.active = false;
2018-01-29 15:47:26 -08:00
});
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
2018-01-29 15:47:26 -08:00
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
callback(null, {
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
activated: activated,
2018-08-28 21:43:25 -07:00
edition: config.edition(),
2018-01-29 15:47:26 -08:00
webadminStatus: gWebadminStatus // only valid when !activated
});
});
});
}