if we download it in the platform start phase, there is no way to give feedback to the user. so it's best to show the restore UI and not redirect to the dashboard.
343 lines
12 KiB
JavaScript
343 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
initialize,
|
|
uninitialize,
|
|
getConfig,
|
|
getLogs,
|
|
|
|
reboot,
|
|
isRebootRequired,
|
|
|
|
onActivated,
|
|
|
|
setupDnsAndCert,
|
|
|
|
prepareDashboardDomain,
|
|
setDashboardDomain,
|
|
updateDashboardDomain,
|
|
renewCerts,
|
|
syncDnsRecords,
|
|
|
|
runSystemChecks
|
|
};
|
|
|
|
const apps = require('./apps.js'),
|
|
appstore = require('./appstore.js'),
|
|
assert = require('assert'),
|
|
AuditSource = require('./auditsource.js'),
|
|
backups = require('./backups.js'),
|
|
BoxError = require('./boxerror.js'),
|
|
branding = require('./branding.js'),
|
|
constants = require('./constants.js'),
|
|
cron = require('./cron.js'),
|
|
debug = require('debug')('box:cloudron'),
|
|
delay = require('delay'),
|
|
dns = require('./dns.js'),
|
|
domains = require('./domains.js'),
|
|
eventlog = require('./eventlog.js'),
|
|
fs = require('fs'),
|
|
mail = require('./mail.js'),
|
|
notifications = require('./notifications.js'),
|
|
path = require('path'),
|
|
paths = require('./paths.js'),
|
|
platform = require('./platform.js'),
|
|
reverseProxy = require('./reverseproxy.js'),
|
|
safe = require('safetydance'),
|
|
services = require('./services.js'),
|
|
settings = require('./settings.js'),
|
|
shell = require('./shell.js'),
|
|
spawn = require('child_process').spawn,
|
|
split = require('split'),
|
|
sysinfo = require('./sysinfo.js'),
|
|
tasks = require('./tasks.js'),
|
|
users = require('./users.js');
|
|
|
|
const REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
|
|
|
|
async function initialize() {
|
|
safe(runStartupTasks(), { debug }); // background
|
|
|
|
await notifyUpdate();
|
|
}
|
|
|
|
async function uninitialize() {
|
|
await cron.stopJobs();
|
|
await platform.stopAllTasks();
|
|
}
|
|
|
|
async function onActivated(options) {
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
debug('onActivated: running post activation tasks');
|
|
|
|
// Starting the platform after a user is available means:
|
|
// 1. mail bounces can now be sent to the cloudron owner
|
|
// 2. the restore code path can run without sudo (since mail/ is non-root)
|
|
await platform.start(options);
|
|
await cron.startJobs();
|
|
|
|
// disable responding to api calls via IP to not leak domain info. this is carefully placed as the last item, so it buys
|
|
// the UI some time to query the dashboard domain in the restore code path
|
|
await delay(30000);
|
|
await reverseProxy.writeDefaultConfig({ activated :true });
|
|
}
|
|
|
|
async function notifyUpdate() {
|
|
const version = safe.fs.readFileSync(paths.VERSION_FILE, 'utf8');
|
|
if (version === constants.VERSION) return;
|
|
|
|
await eventlog.add(eventlog.ACTION_UPDATE_FINISH, AuditSource.CRON, { errorMessage: '', oldVersion: version || 'dev', newVersion: constants.VERSION });
|
|
|
|
const [error] = await safe(tasks.setCompletedByType(tasks.TASK_UPDATE, { error: null }));
|
|
if (error && error.reason !== BoxError.NOT_FOUND) throw error; // when hotfixing, task may not exist
|
|
|
|
safe.fs.writeFileSync(paths.VERSION_FILE, constants.VERSION, 'utf8');
|
|
}
|
|
|
|
// each of these tasks can fail. we will add some routes to fix/re-run them
|
|
async function runStartupTasks() {
|
|
const tasks = [];
|
|
|
|
// stop all the systemd tasks
|
|
tasks.push(platform.stopAllTasks);
|
|
|
|
// this configures collectd to collect backup storage metrics if filesystem is used. This is also triggerd when the settings change with the rest api
|
|
tasks.push(async function () {
|
|
const backupConfig = await settings.getBackupConfig();
|
|
await backups.configureCollectd(backupConfig);
|
|
});
|
|
|
|
// always generate webadmin config since we have no versioning mechanism for the ejs
|
|
tasks.push(async function () {
|
|
if (!settings.dashboardDomain()) return;
|
|
|
|
await reverseProxy.writeDashboardConfig(settings.dashboardDomain());
|
|
});
|
|
|
|
tasks.push(async function () {
|
|
// check activation state and start the platform
|
|
const activated = await users.isActivated();
|
|
|
|
// configure nginx to be reachable by IP when not activated. for the moment, the IP based redirect exists even after domain is setup
|
|
// just in case user forgot or some network error happenned in the middle (then browser refresh takes you to activation page)
|
|
// we remove the config as a simple security measure to not expose IP <-> domain
|
|
if (!activated) {
|
|
debug('runStartupTasks: not activated. generating IP based redirection config');
|
|
return await reverseProxy.writeDefaultConfig({ activated: false });
|
|
}
|
|
|
|
await onActivated({});
|
|
});
|
|
|
|
// we used to run tasks in parallel but simultaneous nginx reloads was causing issues
|
|
for (let i = 0; i < tasks.length; i++) {
|
|
const [error] = await safe(tasks[i]());
|
|
if (error) debug(`Startup task at index ${i} failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async function getConfig() {
|
|
const release = safe.fs.readFileSync('/etc/lsb-release', 'utf-8');
|
|
if (release === null) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
|
|
const ubuntuVersion = release.match(/DISTRIB_DESCRIPTION="(.*)"/)[1];
|
|
|
|
const allSettings = await settings.list();
|
|
|
|
// be picky about what we send out here since this is sent for 'normal' users as well
|
|
return {
|
|
apiServerOrigin: settings.apiServerOrigin(),
|
|
webServerOrigin: settings.webServerOrigin(),
|
|
adminDomain: settings.dashboardDomain(),
|
|
adminFqdn: settings.dashboardFqdn(),
|
|
mailFqdn: settings.mailFqdn(),
|
|
version: constants.VERSION,
|
|
ubuntuVersion,
|
|
isDemo: settings.isDemo(),
|
|
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
|
|
footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER),
|
|
features: appstore.getFeatures(),
|
|
profileLocked: allSettings[settings.DIRECTORY_CONFIG_KEY].lockUserProfiles,
|
|
mandatory2FA: allSettings[settings.DIRECTORY_CONFIG_KEY].mandatory2FA
|
|
};
|
|
}
|
|
|
|
async function reboot() {
|
|
await notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', '');
|
|
|
|
const [error] = await safe(shell.promises.sudo('reboot', [ REBOOT_CMD ], {}));
|
|
if (error) debug('reboot: could not reboot', error);
|
|
}
|
|
|
|
async function isRebootRequired() {
|
|
// https://serverfault.com/questions/92932/how-does-ubuntu-keep-track-of-the-system-restart-required-flag-in-motd
|
|
return fs.existsSync('/var/run/reboot-required');
|
|
}
|
|
|
|
async function runSystemChecks() {
|
|
debug('runSystemChecks: checking status');
|
|
|
|
const checks = [
|
|
checkMailStatus(),
|
|
checkRebootRequired(),
|
|
checkUbuntuVersion()
|
|
];
|
|
|
|
await Promise.allSettled(checks);
|
|
}
|
|
|
|
async function checkMailStatus() {
|
|
const message = await mail.checkConfiguration();
|
|
await notifications.alert(notifications.ALERT_MAIL_STATUS, 'Email is not configured properly', message);
|
|
}
|
|
|
|
async function checkRebootRequired() {
|
|
const rebootRequired = await isRebootRequired();
|
|
await notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', rebootRequired ? 'To finish ubuntu security updates, a reboot is necessary.' : '');
|
|
}
|
|
|
|
async function checkUbuntuVersion() {
|
|
const isXenial = fs.readFileSync('/etc/lsb-release', 'utf-8').includes('16.04');
|
|
if (!isXenial) return;
|
|
|
|
await notifications.alert(notifications.ALERT_UPDATE_UBUNTU, 'Ubuntu upgrade required', 'Ubuntu 16.04 has reached end of life and will not receive security and maintenance updates. Please follow https://docs.cloudron.io/guides/upgrade-ubuntu-18/ to upgrade to Ubuntu 18 at the earliest.');
|
|
}
|
|
|
|
async function getLogs(unit, options) {
|
|
assert.strictEqual(typeof unit, 'string');
|
|
assert(options && typeof options === 'object');
|
|
|
|
assert.strictEqual(typeof options.lines, 'number');
|
|
assert.strictEqual(typeof options.format, 'string');
|
|
assert.strictEqual(typeof options.follow, 'boolean');
|
|
|
|
var lines = options.lines === -1 ? '+1' : options.lines,
|
|
format = options.format || 'json',
|
|
follow = options.follow;
|
|
|
|
debug('Getting logs for %s as %s', unit, format);
|
|
|
|
let args = [ '--lines=' + lines ];
|
|
if (follow) args.push('--follow');
|
|
|
|
// need to handle box.log without subdir
|
|
if (unit === 'box') args.push(path.join(paths.LOG_DIR, 'box.log'));
|
|
else if (unit.startsWith('crash-')) args.push(path.join(paths.CRASH_LOG_DIR, unit.slice(6) + '.log'));
|
|
else throw new BoxError(BoxError.BAD_FIELD, 'No such unit', { field: 'unit' });
|
|
|
|
const cp = spawn('/usr/bin/tail', args);
|
|
|
|
const transformStream = split(function mapper(line) {
|
|
if (format !== 'json') return line + '\n';
|
|
|
|
const data = line.split(' '); // logs are <ISOtimestamp> <msg>
|
|
let timestamp = (new Date(data[0])).getTime();
|
|
if (isNaN(timestamp)) timestamp = 0;
|
|
|
|
return JSON.stringify({
|
|
realtimeTimestamp: timestamp * 1000,
|
|
message: line.slice(data[0].length+1),
|
|
source: unit
|
|
}) + '\n';
|
|
});
|
|
|
|
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
|
|
|
|
cp.stdout.pipe(transformStream);
|
|
|
|
return transformStream;
|
|
}
|
|
|
|
async function prepareDashboardDomain(domain, auditSource) {
|
|
assert.strictEqual(typeof domain, 'string');
|
|
assert.strictEqual(typeof auditSource, 'object');
|
|
|
|
debug(`prepareDashboardDomain: ${domain}`);
|
|
|
|
if (settings.isDemo()) throw new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode');
|
|
|
|
const domainObject = await domains.get(domain);
|
|
if (!domain) throw new BoxError(BoxError.NOT_FOUND, 'No such domain');
|
|
|
|
const fqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject);
|
|
|
|
const result = await apps.list();
|
|
if (result.some(app => app.fqdn === fqdn)) throw new BoxError(BoxError.BAD_STATE, 'Dashboard location conflicts with an existing app');
|
|
|
|
const taskId = await tasks.add(tasks.TASK_SETUP_DNS_AND_CERT, [ constants.DASHBOARD_LOCATION, domain, auditSource ]);
|
|
|
|
tasks.startTask(taskId, {});
|
|
|
|
return taskId;
|
|
}
|
|
|
|
// call this only pre activation since it won't start mail server
|
|
async function setDashboardDomain(domain, auditSource) {
|
|
assert.strictEqual(typeof domain, 'string');
|
|
assert.strictEqual(typeof auditSource, 'object');
|
|
|
|
debug(`setDashboardDomain: ${domain}`);
|
|
|
|
const domainObject = await domains.get(domain);
|
|
if (!domain) throw new BoxError(BoxError.NOT_FOUND, 'No such domain');
|
|
|
|
await reverseProxy.writeDashboardConfig(domain);
|
|
const fqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject);
|
|
|
|
await settings.setDashboardLocation(domain, fqdn);
|
|
|
|
await safe(appstore.updateCloudron({ domain }));
|
|
|
|
await eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain, fqdn });
|
|
}
|
|
|
|
// call this only post activation because it will restart mail server
|
|
async function updateDashboardDomain(domain, auditSource) {
|
|
assert.strictEqual(typeof domain, 'string');
|
|
assert.strictEqual(typeof auditSource, 'object');
|
|
|
|
debug(`updateDashboardDomain: ${domain}`);
|
|
|
|
if (settings.isDemo()) throw new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode');
|
|
|
|
await setDashboardDomain(domain, auditSource);
|
|
|
|
safe(services.rebuildService('turn', auditSource), { debug }); // to update the realm variable
|
|
}
|
|
|
|
async function renewCerts(options, auditSource) {
|
|
assert.strictEqual(typeof options, 'object');
|
|
assert.strictEqual(typeof auditSource, 'object');
|
|
|
|
const taskId = await tasks.add(tasks.TASK_CHECK_CERTS, [ options, auditSource ]);
|
|
tasks.startTask(taskId, {});
|
|
return taskId;
|
|
}
|
|
|
|
async function setupDnsAndCert(subdomain, domain, auditSource, progressCallback) {
|
|
assert.strictEqual(typeof subdomain, 'string');
|
|
assert.strictEqual(typeof domain, 'string');
|
|
assert.strictEqual(typeof auditSource, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const domainObject = await domains.get(domain);
|
|
const dashboardFqdn = dns.fqdn(subdomain, domainObject);
|
|
|
|
const ip = await sysinfo.getServerIp();
|
|
|
|
progressCallback({ message: `Updating DNS of ${dashboardFqdn}` });
|
|
await dns.upsertDnsRecords(subdomain, domain, 'A', [ ip ]);
|
|
progressCallback({ message: `Waiting for DNS of ${dashboardFqdn}` });
|
|
await dns.waitForDnsRecord(subdomain, domain, 'A', ip, { interval: 30000, times: 50000 });
|
|
progressCallback({ message: `Getting certificate of ${dashboardFqdn}` });
|
|
await reverseProxy.ensureCertificate(dns.fqdn(subdomain, domainObject), domain, auditSource);
|
|
}
|
|
|
|
async function syncDnsRecords(options) {
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
const taskId = await tasks.add(tasks.TASK_SYNC_DNS_RECORDS, [ options ]);
|
|
tasks.startTask(taskId, {});
|
|
return taskId;
|
|
}
|