diff --git a/dashboard/src/js/client.js b/dashboard/src/js/client.js index c4b1f7cb7..d2f4db0d7 100644 --- a/dashboard/src/js/client.js +++ b/dashboard/src/js/client.js @@ -82,7 +82,7 @@ var TASK_TYPES = { TASK_BACKUP: 'backup', TASK_UPDATE: 'update', TASK_CHECK_CERTS: 'checkCerts', - TASK_SETUP_DNS_AND_CERT: 'setupDnsAndCert', + TASK_PREPARE_DASHBOARD_LOCATION: 'prepareDashboardLocation', TASK_CLEAN_BACKUPS: 'cleanBackups', TASK_SYNC_EXTERNAL_LDAP: 'syncExternalLdap', TASK_CHANGE_MAIL_LOCATION: 'changeMailLocation', diff --git a/dashboard/src/views/domains.js b/dashboard/src/views/domains.js index d70e3f6ab..856dc4682 100644 --- a/dashboard/src/views/domains.js +++ b/dashboard/src/views/domains.js @@ -649,7 +649,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat tasks: [], refreshTasks: function () { - Client.getTasksByType(TASK_TYPES.TASK_SETUP_DNS_AND_CERT, function (error, tasks) { + Client.getTasksByType(TASK_TYPES.TASK_PREPARE_DASHBOARD_LOCATION, function (error, tasks) { if (error) return console.error(error); $scope.changeDashboard.tasks = tasks.slice(0, 10); if ($scope.changeDashboard.tasks.length && $scope.changeDashboard.tasks[0].active) $scope.changeDashboard.updateStatus(); diff --git a/frontend/src/constants.js b/frontend/src/constants.js index 8334f56b6..6575be6e4 100644 --- a/frontend/src/constants.js +++ b/frontend/src/constants.js @@ -67,7 +67,7 @@ const TASK_TYPES = { TASK_BACKUP: 'backup', TASK_UPDATE: 'update', TASK_CHECK_CERTS: 'checkCerts', - TASK_SETUP_DNS_AND_CERT: 'setupDnsAndCert', + TASK_PREPARE_DASHBOARD_LOCATION: 'prepareDashboardLocation', TASK_CLEAN_BACKUPS: 'cleanBackups', TASK_SYNC_EXTERNAL_LDAP: 'syncExternalLdap', TASK_CHANGE_MAIL_LOCATION: 'changeMailLocation', diff --git a/src/apptask.js b/src/apptask.js index 81ae1cc27..b75a6b039 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -9,7 +9,6 @@ exports = module.exports = { _createAppDir: createAppDir, _deleteAppDir: deleteAppDir, _verifyManifest: verifyManifest, - _waitForDnsPropagation: waitForDnsPropagation }; const apps = require('./apps.js'), @@ -28,7 +27,6 @@ const apps = require('./apps.js'), iputils = require('./iputils.js'), manifestFormat = require('cloudron-manifestformat'), mounts = require('./mounts.js'), - network = require('./network.js'), os = require('os'), path = require('path'), paths = require('./paths.js'), @@ -209,37 +207,6 @@ async function downloadIcon(app) { await updateApp(app, { appStoreIcon }); } -async function waitForDnsPropagation(app) { - assert.strictEqual(typeof app, 'object'); - - if (!constants.CLOUDRON) { - debug('waitForDnsPropagation: Skipping dns propagation check for development'); - return; - } - - const ipv4 = await network.getIPv4(); - const ipv6 = await network.getIPv6(); - - let error; - [error] = await safe(dns.waitForDnsRecord(app.subdomain, app.domain, 'A', ipv4, { times: 240 })); - if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS A Record is not synced yet: ${error.message}`, { ipv4, subdomain: app.subdomain, domain: app.domain }); - if (ipv6) { - [error] = await safe(dns.waitForDnsRecord(app.subdomain, app.domain, 'AAAA', ipv6, { times: 240 })); - if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS AAAA Record is not synced yet: ${error.message}`, { ipv6, subdomain: app.subdomain, domain: app.domain }); - } - - // now wait for redirectDomains and aliasDomains, if any - const allDomains = app.secondaryDomains.concat(app.redirectDomains).concat(app.aliasDomains); - for (const domain of allDomains) { - [error] = await safe(dns.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ipv4, { times: 240 })); - if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS A Record is not synced yet: ${error.message}`, { ipv4, subdomain: domain.subdomain, domain: domain.domain }); - if (ipv6) { - [error] = await safe(dns.waitForDnsRecord(domain.subdomain, domain.domain, 'AAAA', ipv6, { times: 240 })); - if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS AAAA Record is not synced yet: ${error.message}`, { ipv6, subdomain: domain.subdomain, domain: domain.domain }); - } - } -} - async function moveDataDir(app, targetVolumeId, targetVolumePrefix) { assert.strictEqual(typeof app, 'object'); assert(targetVolumeId === null || typeof targetVolumeId === 'string'); @@ -384,7 +351,7 @@ async function install(app, args, progressCallback) { if (!skipDnsSetup) { await progressCallback({ percent: 85, message: 'Waiting for DNS propagation' }); - await exports._waitForDnsPropagation(app); + await dns.waitForLocations([ { subdomain: app.subdomain, domain: app.domain }].concat(app.secondaryDomains).concat(app.redirectDomains).concat(app.aliasDomains), progressCallback); } await progressCallback({ percent: 95, message: 'Configuring reverse proxy' }); @@ -486,7 +453,7 @@ async function changeLocation(app, args, progressCallback) { if (!skipDnsSetup) { await progressCallback({ percent: 80, message: 'Waiting for DNS propagation' }); - await exports._waitForDnsPropagation(app); + await dns.waitForLocations([ { subdomain: app.subdomain, domain: app.domain }].concat(app.secondaryDomains).concat(app.redirectDomains).concat(app.aliasDomains), progressCallback); } await progressCallback({ percent: 90, message: 'Configuring reverse proxy' }); diff --git a/src/dashboard.js b/src/dashboard.js index a88155cb7..ed2f2873e 100644 --- a/src/dashboard.js +++ b/src/dashboard.js @@ -5,8 +5,7 @@ exports = module.exports = { setLocation, clearLocation, - setupDnsAndCert, - + startPrepareLocation, prepareLocation, setupLocation, @@ -23,7 +22,6 @@ const apps = require('./apps.js'), eventlog = require('./eventlog.js'), dns = require('./dns.js'), mailServer = require('./mailserver.js'), - network = require('./network.js'), platform = require('./platform.js'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), @@ -77,7 +75,7 @@ async function getConfig() { }; } -async function prepareLocation(domain, auditSource) { +async function startPrepareLocation(domain, auditSource) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof auditSource, 'object'); @@ -89,12 +87,28 @@ async function prepareLocation(domain, auditSource) { 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_SUBDOMAIN, domain, auditSource ]); + const taskId = await tasks.add(tasks.TASK_PREPARE_DASHBOARD_LOCATION, [ constants.DASHBOARD_SUBDOMAIN, domain, auditSource ]); tasks.startTask(taskId, {}); return taskId; } +async function prepareLocation(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 location = { subdomain, domain }; + const fqdn = dns.fqdn(subdomain, domain); + progressCallback({ percent: 20, message: `Updating DNS of ${fqdn}` }); + await dns.registerLocations([location], { overwriteDns: true }, progressCallback); + progressCallback({ percent: 40, message: `Waiting for DNS of ${fqdn}` }); + await dns.waitForLocations([location], progressCallback); + progressCallback({ percent: 60, message: `Getting certificate of ${fqdn}` }); + await reverseProxy.ensureCertificate(location, {}, auditSource); +} + async function setupLocation(domain, auditSource) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof auditSource, 'object'); @@ -111,25 +125,3 @@ async function setupLocation(domain, auditSource) { await eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain, fqdn }); await platform.onDashboardLocationChanged(auditSource); } - -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 dashboardFqdn = dns.fqdn(subdomain, domain); - - const ipv4 = await network.getIPv4(); - const ipv6 = await network.getIPv6(); - - progressCallback({ percent: 20, message: `Updating DNS of ${dashboardFqdn}` }); - await dns.upsertDnsRecords(subdomain, domain, 'A', [ ipv4 ]); - if (ipv6) await dns.upsertDnsRecords(subdomain, domain, 'AAAA', [ ipv6 ]); - progressCallback({ percent: 40, message: `Waiting for DNS of ${dashboardFqdn}` }); - await dns.waitForDnsRecord(subdomain, domain, 'A', ipv4, { interval: 30000, times: 50000 }); - if (ipv6) await dns.waitForDnsRecord(subdomain, domain, 'AAAA', ipv6, { interval: 30000, times: 50000 }); - progressCallback({ percent: 60, message: `Getting certificate of ${dashboardFqdn}` }); - const location = { subdomain, domain, fqdn: dashboardFqdn, type: apps.LOCATION_TYPE_DASHBOARD, certificate: null }; - await reverseProxy.ensureCertificate(location, {}, auditSource); -} diff --git a/src/dns.js b/src/dns.js index fc4ac51c7..a36383fc1 100644 --- a/src/dns.js +++ b/src/dns.js @@ -9,6 +9,7 @@ module.exports = exports = { removeDnsRecords, waitForDnsRecord, + waitForLocations, validateHostname, @@ -181,6 +182,28 @@ async function waitForDnsRecord(subdomain, domain, type, value, options) { await api(domainObject.provider).wait(domainObject, subdomain, type, value, options); } +async function waitForLocations(locations, progressCallback) { + assert(Array.isArray(locations)); + assert.strictEqual(typeof progressCallback, 'function'); + + if (constants.TEST) return; + + const ipv4 = await network.getIPv4(); + const ipv6 = await network.getIPv6(); + + for (const location of locations) { + const { subdomain, domain } = location; + progressCallback({ message: `waiting for propagation of: ${fqdn(subdomain, domain)}` }); + + const [error] = await safe(waitForDnsRecord(subdomain, domain, 'A', ipv4, { times: 240 })); + if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS A Record is not synced yet: ${error.message}`, { ipv4, subdomain, domain }); + if (ipv6) { + const [error] = await safe(waitForDnsRecord(subdomain, domain, 'AAAA', ipv6, { times: 240 })); + if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS AAAA Record is not synced yet: ${error.message}`, { ipv6, subdomain, domain }); + } + } +} + function makeWildcard(fqdn) { assert.strictEqual(typeof fqdn, 'string'); diff --git a/src/mailserver.js b/src/mailserver.js index b8541dbad..17ff3af01 100644 --- a/src/mailserver.js +++ b/src/mailserver.js @@ -26,7 +26,6 @@ const assert = require('assert'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), crypto = require('crypto'), - dashboard = require('./dashboard.js'), debug = require('debug')('box:mailserver'), dns = require('./dns.js'), docker = require('./docker.js'), @@ -265,14 +264,17 @@ async function changeLocation(auditSource, progressCallback) { assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof progressCallback, 'function'); - const { fqdn, domain, subdomain } = await getLocation(); + const location = await getLocation(); + const fqdn = dns.fqdn(location.subdomain, location.domain); let progress = 20; progressCallback({ percent: progress, message: `Setting up DNS of certs of mail server ${fqdn}` }); - await dashboard.setupDnsAndCert(subdomain, domain, auditSource, (progress) => progressCallback({ message: progress.message })); // remove the percent - const allDomains = await domains.list(); + await dns.registerLocations([location], { overwriteDns: true }, progressCallback); + await dns.waitForLocations([location], progressCallback); + await reverseProxy.ensureCertificate(location, {}, auditSource); + const allDomains = await domains.list(); for (let idx = 0; idx < allDomains.length; idx++) { const domainObject = allDomains[idx]; progressCallback({ percent: progress, message: `Updating DNS of ${domainObject.domain}` }); diff --git a/src/provision.js b/src/provision.js index 1672de1d1..316e0f85c 100644 --- a/src/provision.js +++ b/src/provision.js @@ -14,6 +14,7 @@ const assert = require('assert'), 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('fs'), @@ -73,14 +74,17 @@ async function setupTask(domain, auditSource) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof auditSource, 'object'); + const location = { subdomain: constants.DASHBOARD_SUBDOMAIN, domain }; try { - await dashboard.setupDnsAndCert(constants.DASHBOARD_SUBDOMAIN, domain, auditSource, (progress) => setProgress('setup', progress.message)); + 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(domain, auditSource); setProgress('setup', 'Done'), await eventlog.add(eventlog.ACTION_PROVISION, auditSource, {}); } catch (error) { - debug(`setupTask: error ${error.message}`); + debug('setupTask: error. %o', error); gStatus.setup.errorMessage = error.message; } @@ -178,9 +182,13 @@ async function restoreTask(backupConfig, remotePath, ipv4Config, options, auditS await network.setIPv4Config(ipv4Config); await reverseProxy.restoreFallbackCertificates(); - const { subdomain:dashboardSubdomain, domain:dashboardDomain } = await dashboard.getLocation(); // load this fresh from after the backup.restore - if (!options.skipDnsSetup) await dashboard.setupDnsAndCert(dashboardSubdomain, dashboardDomain, auditSource, (progress) => setProgress('restore', progress.message)); - await dashboard.setLocation(dashboardDomain, auditSource); + 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.setLocation(location.domain, auditSource); await backups.setConfig(backupConfig); await eventlog.add(eventlog.ACTION_RESTORE, auditSource, { remotePath }); diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 0cf56cfc9..cdb5a84ab 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -246,10 +246,10 @@ async function restoreFallbackCertificates() { function getAppLocationsSync(app) { assert.strictEqual(typeof app, 'object'); - return [{ domain: app.domain, fqdn: app.fqdn, certificate: app.certificate, type: apps.LOCATION_TYPE_PRIMARY }] - .concat(app.secondaryDomains.map(sd => { return { domain: sd.domain, certificate: sd.certificate, fqdn: sd.fqdn, type: apps.LOCATION_TYPE_SECONDARY }; })) - .concat(app.redirectDomains.map(rd => { return { domain: rd.domain, certificate: rd.certificate, fqdn: rd.fqdn, type: apps.LOCATION_TYPE_REDIRECT }; })) - .concat(app.aliasDomains.map(ad => { return { domain: ad.domain, certificate: ad.certificate, fqdn: ad.fqdn, type: apps.LOCATION_TYPE_ALIAS }; })); + return [{ domain: app.domain, subdomain: app.subdomain, fqdn: app.fqdn, certificate: app.certificate, type: apps.LOCATION_TYPE_PRIMARY }] + .concat(app.secondaryDomains.map(sd => { return { domain: sd.domain, subdomain: sd.subdomain, certificate: sd.certificate, fqdn: sd.fqdn, type: apps.LOCATION_TYPE_SECONDARY }; })) + .concat(app.redirectDomains.map(rd => { return { domain: rd.domain, subdomain: rd.subdomain, certificate: rd.certificate, fqdn: rd.fqdn, type: apps.LOCATION_TYPE_REDIRECT }; })) + .concat(app.aliasDomains.map(ad => { return { domain: ad.domain, subdomain: ad.subdomain, certificate: ad.certificate, fqdn: ad.fqdn, type: apps.LOCATION_TYPE_ALIAS }; })); } function getAcmeCertificateNameSync(fqdn, domainObject) { @@ -415,7 +415,7 @@ async function ensureCertificate(location, options, auditSource) { const domainObject = await domains.get(location.domain); if (!domainObject) throw new BoxError(BoxError.NOT_FOUND, `${location.domain} not found`); - const fqdn = location.fqdn; + const fqdn = dns.fqdn(location.subdomain, location.domain); if (location.certificate) { // user certificate debug(`ensureCertificate: ${fqdn} will use user certs`); diff --git a/src/routes/dashboard.js b/src/routes/dashboard.js index 3e96403d0..6650c57f8 100644 --- a/src/routes/dashboard.js +++ b/src/routes/dashboard.js @@ -3,7 +3,7 @@ exports = module.exports = { getConfig, - prepareLocation, + startPrepareLocation, setupLocation }; @@ -21,10 +21,10 @@ async function getConfig(req, res, next) { next(new HttpSuccess(200, cloudronConfig)); } -async function prepareLocation(req, res, next) { +async function startPrepareLocation(req, res, next) { if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string')); - const [error, taskId] = await safe(dashboard.prepareLocation(req.body.domain, AuditSource.fromRequest(req))); + const [error, taskId] = await safe(dashboard.startPrepareLocation(req.body.domain, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId })); diff --git a/src/server.js b/src/server.js index d37baa2ce..596497f02 100644 --- a/src/server.js +++ b/src/server.js @@ -116,7 +116,7 @@ async function initializeExpressSync() { // config route for dashboard that any auth user (not just admin) can access router.get ('/api/v1/dashboard/config', token, authorizeUser, routes.dashboard.getConfig); - router.post('/api/v1/dashboard/prepare_location', json, token, authorizeAdmin, routes.dashboard.prepareLocation); + router.post('/api/v1/dashboard/prepare_location', json, token, authorizeAdmin, routes.dashboard.startPrepareLocation); router.post('/api/v1/dashboard/location', json, token, authorizeAdmin, routes.dashboard.setupLocation); // system (vm/server) diff --git a/src/tasks.js b/src/tasks.js index 552410d12..75d27cd60 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -24,7 +24,7 @@ exports = module.exports = { TASK_UPDATE: 'update', TASK_CHECK_CERTS: 'checkCerts', TASK_SYNC_DYNDNS: 'syncDyndns', - TASK_SETUP_DNS_AND_CERT: 'setupDnsAndCert', + TASK_PREPARE_DASHBOARD_LOCATION: 'prepareDashboardLocation', TASK_CLEAN_BACKUPS: 'cleanBackups', TASK_SYNC_EXTERNAL_LDAP: 'syncExternalLdap', TASK_CHANGE_MAIL_LOCATION: 'changeMailLocation', @@ -174,8 +174,6 @@ function startTask(id, options, callback) { gTasks[id] = shell.sudo('startTask', [ START_TASK_CMD, id, logFile, options.nice || 0, options.memoryLimit || 400 ], sudoOptions, async function (sudoError) { if (!gTasks[id]) return; // ignore task exit since we are shutting down. see stopAllTasks - console.log(sudoError) - const code = sudoError ? sudoError.code : 0; debug(`startTask: ${id} completed with code ${code}`); diff --git a/src/taskworker.js b/src/taskworker.js index c84844bea..5d1f73c41 100755 --- a/src/taskworker.js +++ b/src/taskworker.js @@ -24,7 +24,7 @@ const TASKS = { // indexed by task type backup: backuptask.fullBackup, update: updater.update, checkCerts: reverseProxy.checkCerts, - setupDnsAndCert: dashboard.setupDnsAndCert, + prepareDashboardLocation: dashboard.prepareLocation, cleanBackups: backupCleaner.run, syncExternalLdap: externalLdap.sync, changeMailLocation: mailServer.changeLocation,