diff --git a/src/apps.js b/src/apps.js index 883aaa7c1..f43e31a1f 100644 --- a/src/apps.js +++ b/src/apps.js @@ -1137,35 +1137,28 @@ function setAutomaticUpdate(app, enable, auditSource, callback) { }); } -function setReverseProxyConfig(app, reverseProxyConfig, auditSource, callback) { +async function setReverseProxyConfig(app, reverseProxyConfig, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof reverseProxyConfig, 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); reverseProxyConfig = _.extend({ robotsTxt: null, csp: null }, reverseProxyConfig); const appId = app.id; let error = validateCsp(reverseProxyConfig.csp); - if (error) return callback(error); + if (error) throw error; error = validateRobotsTxt(reverseProxyConfig.robotsTxt); - if (error) return callback(error); + if (error) throw error; - reverseProxy.writeAppConfig(_.extend({}, app, { reverseProxyConfig }), function (error) { - if (error) return callback(error); + await reverseProxy.writeAppConfig(_.extend({}, app, { reverseProxyConfig })); - appdb.update(appId, { reverseProxyConfig }, function (error) { - if (error) return callback(error); + await util.promisify(appdb.update)(appId, { reverseProxyConfig }); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, reverseProxyConfig }); - - callback(); - }); - }); + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, reverseProxyConfig }); } -function setCertificate(app, data, auditSource, callback) { +async function setCertificate(app, data, auditSource) { assert.strictEqual(typeof app, 'object'); assert(data && typeof data === 'object'); assert.strictEqual(typeof auditSource, 'object'); @@ -1174,22 +1167,15 @@ function setCertificate(app, data, auditSource, callback) { const appId = app.id; const { location, domain, cert, key } = data; - domains.get(domain, function (error, domainObject) { - if (error) return callback(error); + const domainObject = await domains.get(domain); - if (cert && key) { - error = reverseProxy.validateCertificate(location, domainObject, { cert, key }); - if (error) return callback(error); - } + if (cert && key) { + const error = reverseProxy.validateCertificate(location, domainObject, { cert, key }); + if (error) throw error; + } - reverseProxy.setAppCertificateSync(location, domainObject, { cert, key }, function (error) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cert, key }); - - callback(); - }); - }); + await reverseProxy.setAppCertificateSync(location, domainObject, { cert, key }); + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cert, key }); } function setLocation(app, data, auditSource, callback) { diff --git a/src/apptask.js b/src/apptask.js index 6d980cdae..646920767 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -16,6 +16,7 @@ const appdb = require('./appdb.js'), apps = require('./apps.js'), assert = require('assert'), async = require('async'), + auditSource = require('./auditsource.js'), backuptask = require('./backuptask.js'), BoxError = require('./boxerror.js'), collectd = require('./collectd.js'), @@ -24,7 +25,6 @@ const appdb = require('./appdb.js'), df = require('@sindresorhus/df'), dns = require('./dns.js'), docker = require('./docker.js'), - domains = require('./domains.js'), ejs = require('ejs'), fs = require('fs'), iputils = require('./iputils.js'), @@ -93,28 +93,6 @@ function allocateContainerIp(app, callback) { }, callback); } -function configureReverseProxy(app, callback) { - assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof callback, 'function'); - - reverseProxy.configureApp(app, { userId: null, username: 'apptask' }, function (error) { - if (error) return callback(error); - - callback(null); - }); -} - -function unconfigureReverseProxy(app, callback) { - assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof callback, 'function'); - - reverseProxy.unconfigureApp(app, function (error) { - if (error) return callback(error); - - callback(null); - }); -} - function createContainer(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); @@ -401,7 +379,7 @@ function install(app, args, progressCallback, callback) { // teardown for re-installs progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }), - unconfigureReverseProxy.bind(null, app), + reverseProxy.unconfigureApp.bind(null, app), deleteContainers.bind(null, app, { managedOnly: true }), function teardownAddons(next) { // when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords @@ -493,7 +471,7 @@ function install(app, args, progressCallback, callback) { }, progressCallback.bind(null, { percent: 95, message: 'Configuring reverse proxy' }), - configureReverseProxy.bind(null, app), + reverseProxy.configureApp.bind(null, app, auditSource.APPTASK), progressCallback.bind(null, { percent: 100, message: 'Done' }), updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }) @@ -573,7 +551,7 @@ function changeLocation(app, args, progressCallback, callback) { async.series([ progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }), - unconfigureReverseProxy.bind(null, app), + reverseProxy.unconfigureApp.bind(null, app), deleteContainers.bind(null, app, { managedOnly: true }), function (next) { let obsoleteDomains = oldConfig.alternateDomains.filter(function (o) { @@ -621,7 +599,7 @@ function changeLocation(app, args, progressCallback, callback) { }, progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }), - configureReverseProxy.bind(null, app), + reverseProxy.configureApp.bind(null, app, auditSource.APPTASK), progressCallback.bind(null, { percent: 100, message: 'Done' }), updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }) @@ -683,7 +661,7 @@ function configure(app, args, progressCallback, callback) { async.series([ progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }), - unconfigureReverseProxy.bind(null, app), + reverseProxy.unconfigureApp.bind(null, app), deleteContainers.bind(null, app, { managedOnly: true }), progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }), @@ -705,7 +683,7 @@ function configure(app, args, progressCallback, callback) { startApp.bind(null, app), progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }), - configureReverseProxy.bind(null, app), + reverseProxy.configureApp.bind(null, app, auditSource.APPTASK), progressCallback.bind(null, { percent: 100, message: 'Done' }), updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }) @@ -809,10 +787,10 @@ function update(app, args, progressCallback, callback) { startApp.bind(null, app), progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }), - function (next) { - if (!httpPathsChanged && !proxyAuthChanged && !httpPortChanged) return next(); + async function () { + if (!httpPathsChanged && !proxyAuthChanged && !httpPortChanged) return; - configureReverseProxy(app, next); + await reverseProxy.configureApp(app, auditSource.APPTASK); }, progressCallback.bind(null, { percent: 100, message: 'Done' }), @@ -848,7 +826,7 @@ function start(app, args, progressCallback, callback) { // stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings progressCallback.bind(null, { percent: 80, message: 'Configuring reverse proxy' }), - configureReverseProxy.bind(null, app), + reverseProxy.configureApp.bind(null, app, auditSource.APPTASK), progressCallback.bind(null, { percent: 100, message: 'Done' }), updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }) @@ -917,7 +895,7 @@ function uninstall(app, args, progressCallback, callback) { async.series([ progressCallback.bind(null, { percent: 20, message: 'Deleting container' }), - unconfigureReverseProxy.bind(null, app), + reverseProxy.unconfigureApp.bind(null, app), deleteContainers.bind(null, app, {}), progressCallback.bind(null, { percent: 30, message: 'Teardown addons' }), diff --git a/src/auditsource.js b/src/auditsource.js index 46d55272b..8fdb17259 100644 --- a/src/auditsource.js +++ b/src/auditsource.js @@ -5,6 +5,7 @@ exports = module.exports = { HEALTH_MONITOR: { userId: null, username: 'healthmonitor' }, EXTERNAL_LDAP_TASK: { userId: null, username: 'externalldap' }, EXTERNAL_LDAP_AUTO_CREATE: { userId: null, username: 'externalldap' }, + APPTASK: { userId: null, username: 'apptask' }, fromRequest }; diff --git a/src/cloudron.js b/src/cloudron.js index 153bd5282..31bff1cb4 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -33,6 +33,7 @@ const apps = require('./apps.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'), @@ -89,7 +90,10 @@ function onActivated(options, callback) { 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 - (done) => setTimeout(() => reverseProxy.writeDefaultConfig({ activated :true }, done), 30000) + async () => { + await delay(30000); + await reverseProxy.writeDefaultConfig({ activated :true }); + } ], callback); } @@ -121,27 +125,25 @@ function runStartupTasks() { }, // always generate webadmin config since we have no versioning mechanism for the ejs - function (callback) { - if (!settings.dashboardDomain()) return callback(); + async function () { + if (!settings.dashboardDomain()) return; - reverseProxy.writeDashboardConfig(settings.dashboardDomain(), callback); + await reverseProxy.writeDashboardConfig(settings.dashboardDomain()); }, // check activation state and start the platform - function (callback) { - util.callbackify(users.isActivated)(function (error, activated) { - if (error) return callback(error); + async function () { + 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 reverseProxy.writeDefaultConfig({ activated: false }, callback); - } + // 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 }); + } - onActivated({}, callback); - }); + await util.promisify(onActivated)({}); } ]; @@ -317,7 +319,7 @@ function setDashboardDomain(domain, auditSource, callback) { domainsGet(domain, function (error, domainObject) { if (error) return callback(error); - reverseProxy.writeDashboardConfig(domain, function (error) { + util.callbackify(reverseProxy.writeDashboardConfig)(domain, function (error) { if (error) return callback(error); const fqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject); diff --git a/src/docker.js b/src/docker.js index 642abfe59..c8f9403d2 100644 --- a/src/docker.js +++ b/src/docker.js @@ -41,7 +41,6 @@ const apps = require('./apps.js'), constants = require('./constants.js'), debug = require('debug')('box:docker'), Docker = require('dockerode'), - os = require('os'), path = require('path'), reverseProxy = require('./reverseproxy.js'), services = require('./services.js'), @@ -228,7 +227,7 @@ function getAddonMounts(app, callback) { const addons = app.manifest.addons; if (!addons) return callback(null, mounts); - async.eachSeries(Object.keys(addons), function (addon, iteratorDone) { + async.eachSeries(Object.keys(addons), async function (addon) { switch (addon) { case 'localstorage': mounts.push({ @@ -238,31 +237,28 @@ function getAddonMounts(app, callback) { ReadOnly: false }); - return iteratorDone(); - case 'tls': - reverseProxy.getCertificatePath(app.fqdn, app.domain, function (error, bundle) { - if (error) return iteratorDone(error); + return; + case 'tls': { + const bundle = await reverseProxy.getCertificatePath(app.fqdn, app.domain); - mounts.push({ - Target: '/etc/certs/tls_cert.pem', - Source: bundle.certFilePath, - Type: 'bind', - ReadOnly: true - }); + mounts.push({ + Target: '/etc/certs/tls_cert.pem', + Source: bundle.certFilePath, + Type: 'bind', + ReadOnly: true + }); - mounts.push({ - Target: '/etc/certs/tls_key.pem', - Source: bundle.keyFilePath, - Type: 'bind', - ReadOnly: true - }); - - iteratorDone(); + mounts.push({ + Target: '/etc/certs/tls_key.pem', + Source: bundle.keyFilePath, + Type: 'bind', + ReadOnly: true }); return; + } default: - iteratorDone(); + return; } }, function (error) { callback(error, mounts); diff --git a/src/domains.js b/src/domains.js index db45007b8..b3899f454 100644 --- a/src/domains.js +++ b/src/domains.js @@ -141,7 +141,7 @@ async function add(domain, data, auditSource) { let error = reverseProxy.validateCertificate('test', { domain, config }, fallbackCertificate); if (error) throw error; } else { - fallbackCertificate = reverseProxy.generateFallbackCertificateSync(domain); + fallbackCertificate = await reverseProxy.generateFallbackCertificate(domain); if (fallbackCertificate.error) throw fallbackCertificate.error; } diff --git a/src/mail.js b/src/mail.js index a7d07ec41..dcbdb1d51 100644 --- a/src/mail.js +++ b/src/mail.js @@ -668,7 +668,8 @@ function configureMail(mailFqdn, mailDomain, serviceConfig, callback) { const memory = system.getMemoryAllocation(memoryLimit); const cloudronToken = hat(8 * 128), relayToken = hat(8 * 128); - reverseProxy.getCertificatePath(mailFqdn, mailDomain, function (error, bundle) { + const getCertificatePath = util.callbackify(reverseProxy.getCertificatePath); + getCertificatePath(mailFqdn, mailDomain, function (error, bundle) { if (error) return callback(error); const dhparamsFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/dhparams.pem'); diff --git a/src/reverseproxy.js b/src/reverseproxy.js index c645d2bad..b7c2e8af4 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -4,7 +4,7 @@ exports = module.exports = { setAppCertificate, setFallbackCertificate, - generateFallbackCertificateSync, + generateFallbackCertificate, validateCertificate, @@ -47,7 +47,6 @@ const acme2 = require('./acme2.js'), os = require('os'), path = require('path'), paths = require('./paths.js'), - rimraf = require('rimraf'), safe = require('safetydance'), settings = require('./settings.js'), shell = require('./shell.js'), @@ -58,8 +57,6 @@ const acme2 = require('./acme2.js'), const NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' }); const RESTART_SERVICE_CMD = path.join(__dirname, 'scripts/restartservice.sh'); -const domainsGet = util.callbackify(domains.get), - domainsList = util.callbackify(domains.list); function nginxLocation(s) { if (!s.startsWith('!')) return s; @@ -185,17 +182,14 @@ function validateCertificate(location, domainObject, certificate) { return null; } -function reload(callback) { - if (constants.TEST) return callback(null); +async function reload() { + if (constants.TEST) return; - shell.sudo('reload', [ RESTART_SERVICE_CMD, 'nginx' ], {}, function (error) { - if (error) return callback(new BoxError(BoxError.NGINX_ERROR, `Error reloading nginx: ${error.message}`)); - - callback(null); - }); + const [error] = await safe(shell.promises.sudo('reload', [ RESTART_SERVICE_CMD, 'nginx' ], {})); + if (error) throw new BoxError(BoxError.NGINX_ERROR, `Error reloading nginx: ${error.message}`); } -function generateFallbackCertificateSync(domain) { +async function generateFallbackCertificate(domain) { assert.strictEqual(typeof domain, 'string'); const certFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.cert`); @@ -237,21 +231,15 @@ async function setFallbackCertificate(domain, fallback) { if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) throw new BoxError(BoxError.FS_ERROR, safe.error.message); // TODO: maybe the cert is being used by the mail container - await util.promisify(reload)(); + await reload(); } -function restoreFallbackCertificates(callback) { - assert.strictEqual(typeof callback, 'function'); +async function restoreFallbackCertificates() { + const result = await domains.list(); - domainsList(function (error, result) { - if (error) return callback(error); - - result.forEach(function (domain) { - if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain.domain}.host.cert`), domain.fallbackCertificate.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); - if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain.domain}.host.key`), domain.fallbackCertificate.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); - }); - - callback(null); + result.forEach(function (domain) { + if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain.domain}.host.cert`), domain.fallbackCertificate.cert)) throw new BoxError(BoxError.FS_ERROR, safe.error.message); + if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain.domain}.host.key`), domain.fallbackCertificate.key)) throw new BoxError(BoxError.FS_ERROR, safe.error.message); }); } @@ -294,48 +282,44 @@ function getAcmeCertificatePathSync(vhost, domainObject) { return { certName, certFilePath, keyFilePath, csrFilePath, acmeChallengesDir }; } -function setAppCertificate(location, domainObject, certificate, callback) { +async function setAppCertificate(location, domainObject, certificate) { assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof certificate, 'object'); - assert.strictEqual(typeof callback, 'function'); const fqdn = dns.fqdn(location, domainObject); const { certFilePath, keyFilePath } = getAppCertificatePathSync(fqdn); if (certificate.cert && certificate.key) { - if (!safe.fs.writeFileSync(certFilePath, certificate.cert)) return callback(safe.error); - if (!safe.fs.writeFileSync(keyFilePath, certificate.key)) return callback(safe.error); + if (!safe.fs.writeFileSync(certFilePath, certificate.cert)) throw safe.error; + if (!safe.fs.writeFileSync(keyFilePath, certificate.key)) throw safe.error; } else { // remove existing cert/key if (!safe.fs.unlinkSync(certFilePath)) debug(`Error removing cert: ${safe.error.message}`); if (!safe.fs.unlinkSync(keyFilePath)) debug(`Error removing key: ${safe.error.message}`); } - reload(callback); + await reload(); } -function getCertificatePath(fqdn, domain, callback) { +async function getCertificatePath(fqdn, domain) { assert.strictEqual(typeof fqdn, 'string'); assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); // 1. user cert always wins // 2. if using fallback provider, return that cert // 3. look for LE certs - domainsGet(domain, function (error, domainObject) { - if (error) return callback(error); + const domainObject = await domains.get(domain); - const appCertPath = getAppCertificatePathSync(fqdn); // user cert always wins - if (fs.existsSync(appCertPath.certFilePath) && fs.existsSync(appCertPath.keyFilePath)) return callback(null, appCertPath); + const appCertPath = getAppCertificatePathSync(fqdn); // user cert always wins + if (fs.existsSync(appCertPath.certFilePath) && fs.existsSync(appCertPath.keyFilePath)) return appCertPath; - if (domainObject.tlsConfig.provider === 'fallback') return callback(null, getFallbackCertificatePathSync(domain)); + if (domainObject.tlsConfig.provider === 'fallback') return getFallbackCertificatePathSync(domain); - const acmeCertPath = getAcmeCertificatePathSync(fqdn, domainObject); - if (fs.existsSync(acmeCertPath.certFilePath) && fs.existsSync(acmeCertPath.keyFilePath)) return callback(null, acmeCertPath); + const acmeCertPath = getAcmeCertificatePathSync(fqdn, domainObject); + if (fs.existsSync(acmeCertPath.certFilePath) && fs.existsSync(acmeCertPath.keyFilePath)) return acmeCertPath; - return callback(null, getFallbackCertificatePathSync(domain)); - }); + return getFallbackCertificatePathSync(domain); } async function checkAppCertificate(vhost, domainObject) { @@ -394,70 +378,63 @@ async function updateCertBlobs(vhost, domainObject) { await blobs.set(`${blobs.CERT_PREFIX}-${certName}.csr`, csr); } -function ensureCertificate(vhost, domain, auditSource, callback) { +async function ensureCertificate(vhost, domain, auditSource) { assert.strictEqual(typeof vhost, 'string'); assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - domainsGet(domain, async function (error, domainObject) { - if (error) return callback(error); + const domainObject = await domains.get(domain); - let bundle; - [error, bundle] = await safe(checkAppCertificate(vhost, domainObject)); - if (error) return callback(error); - if (bundle) return callback(null, bundle, { renewed: false }); + let bundle = await checkAppCertificate(vhost, domainObject); + if (bundle) return { bundle, renewed: false }; - if (domainObject.tlsConfig.provider === 'fallback') { - debug(`ensureCertificate: ${vhost} will use fallback certs`); + if (domainObject.tlsConfig.provider === 'fallback') { + debug(`ensureCertificate: ${vhost} will use fallback certs`); - return callback(null, getFallbackCertificatePathSync(domain), { renewed: false }); - } + return { bundle: getFallbackCertificatePathSync(domain), renewed: false }; + } - const { acmeApi, apiOptions } = await getAcmeApi(domainObject); - let notAfter = null; + const { acmeApi, apiOptions } = await getAcmeApi(domainObject); + let notAfter = null; - const [, currentBundle] = await safe(checkAcmeCertificate(vhost, domainObject)); - if (currentBundle) { - debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`); - notAfter = getExpiryDate(currentBundle.certFilePath); - const isExpiring = (notAfter - new Date()) <= (30 * 24 * 60 * 60 * 1000); // expiring in a month - if (!isExpiring && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle, { renewed: false }); - debug(`ensureCertificate: ${vhost} cert requires renewal`); - } else { - debug(`ensureCertificate: ${vhost} cert does not exist`); - } + const [, currentBundle] = await safe(checkAcmeCertificate(vhost, domainObject)); + if (currentBundle) { + debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`); + notAfter = getExpiryDate(currentBundle.certFilePath); + const isExpiring = (notAfter - new Date()) <= (30 * 24 * 60 * 60 * 1000); // expiring in a month + if (!isExpiring && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return { bundle: currentBundle, renewed: false }; + debug(`ensureCertificate: ${vhost} cert requires renewal`); + } else { + debug(`ensureCertificate: ${vhost} cert does not exist`); + } - debug('ensureCertificate: getting certificate for %s with options %j', vhost, _.omit(apiOptions, 'accountKeyPem')); + debug('ensureCertificate: getting certificate for %s with options %j', vhost, _.omit(apiOptions, 'accountKeyPem')); - const acmePaths = getAcmeCertificatePathSync(vhost, domainObject); - acmeApi.getCertificate(vhost, domain, acmePaths, apiOptions, async function (error) { - debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${acmePaths.certFilePath || 'null'}`); + const acmePaths = getAcmeCertificatePathSync(vhost, domainObject); + let [error] = await safe(util.promisify(acmeApi.getCertificate)(vhost, domain, acmePaths, apiOptions)); + debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${acmePaths.certFilePath || 'null'}`); - await safe(eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '', notAfter })); + await safe(eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '', notAfter })); - if (error && currentBundle && (notAfter - new Date() > 0)) { // still some life left in this certificate - debug('ensureCertificate: continue using existing bundle since renewal failed'); - return callback(null, currentBundle, { renewed: false }); - } + if (error && currentBundle && (notAfter - new Date() > 0)) { // still some life left in this certificate + debug('ensureCertificate: continue using existing bundle since renewal failed'); + return { bundle: currentBundle, renewed: false }; + } - if (!error) { - [error] = await safe(updateCertBlobs(vhost, domainObject)); - if (!error) return callback(null, { certFilePath: acmePaths.certFilePath, keyFilePath: acmePaths.keyFilePath }, { renewed: true }); - } + if (!error) { + [error] = await safe(updateCertBlobs(vhost, domainObject)); + if (!error) return { bundle: { certFilePath: acmePaths.certFilePath, keyFilePath: acmePaths.keyFilePath }, renewed: true }; + } - debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`); + debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`); - callback(null, getFallbackCertificatePathSync(domain), { renewed: false }); - }); - }); + return { bundle: getFallbackCertificatePathSync(domain), renewed: false }; } -function writeDashboardNginxConfig(bundle, configFileName, vhost, callback) { +async function writeDashboardNginxConfig(bundle, configFileName, vhost) { assert.strictEqual(typeof bundle, 'object'); assert.strictEqual(typeof configFileName, 'string'); assert.strictEqual(typeof vhost, 'string'); - assert.strictEqual(typeof callback, 'function'); const data = { sourceDir: path.resolve(__dirname, '..'), @@ -473,35 +450,29 @@ function writeDashboardNginxConfig(bundle, configFileName, vhost, callback) { const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data); const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName); - if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(new BoxError(BoxError.FS_ERROR, safe.error)); + if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) throw new BoxError(BoxError.FS_ERROR, safe.error); - reload(callback); + await reload(); } -function writeDashboardConfig(domain, callback) { +async function writeDashboardConfig(domain) { assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); debug(`writeDashboardConfig: writing admin config for ${domain}`); - domainsGet(domain, function (error, domainObject) { - if (error) return callback(error); + const domainObject = await domains.get(domain); - const dashboardFqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject); + const dashboardFqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject); - getCertificatePath(dashboardFqdn, domainObject.domain, function (error, bundle) { - if (error) return callback(error); + const bundle = await getCertificatePath(dashboardFqdn, domainObject.domain); - writeDashboardNginxConfig(bundle, `${dashboardFqdn}.conf`, dashboardFqdn, callback); - }); - }); + await writeDashboardNginxConfig(bundle, `${dashboardFqdn}.conf`, dashboardFqdn); } -function writeAppNginxConfig(app, fqdn, bundle, callback) { +async function writeAppNginxConfig(app, fqdn, bundle) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof fqdn, 'string'); assert.strictEqual(typeof bundle, 'object'); - assert.strictEqual(typeof callback, 'function'); var sourceDir = path.resolve(__dirname, '..'); var endpoint = 'app'; @@ -543,17 +514,16 @@ function writeAppNginxConfig(app, fqdn, bundle, callback) { if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) { debug('Error creating nginx config for "%s" : %s', app.fqdn, safe.error.message); - return callback(new BoxError(BoxError.FS_ERROR, safe.error)); + throw new BoxError(BoxError.FS_ERROR, safe.error); } - reload(callback); + await reload(); } -function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) { +async function writeAppRedirectNginxConfig(app, fqdn, bundle) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof fqdn, 'string'); assert.strictEqual(typeof bundle, 'object'); - assert.strictEqual(typeof callback, 'function'); const data = { sourceDir: path.resolve(__dirname, '..'), @@ -577,15 +547,14 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) { if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) { debug('Error creating nginx redirect config for "%s" : %s', app.fqdn, safe.error.message); - return callback(new BoxError(BoxError.FS_ERROR, safe.error)); + throw new BoxError(BoxError.FS_ERROR, safe.error); } - reload(callback); + await reload(); } -function writeAppConfig(app, callback) { +async function writeAppConfig(app) { assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof callback, 'function'); let appDomains = []; appDomains.push({ domain: app.domain, fqdn: app.fqdn, type: 'primary' }); @@ -598,25 +567,22 @@ function writeAppConfig(app, callback) { appDomains.push({ domain: aliasDomain.domain, fqdn: aliasDomain.fqdn, type: 'alias' }); }); - async.eachSeries(appDomains, function (appDomain, iteratorDone) { - getCertificatePath(appDomain.fqdn, appDomain.domain, function (error, bundle) { - if (error) return iteratorDone(error); + for (const appDomain of appDomains) { + const bundle = await getCertificatePath(appDomain.fqdn, appDomain.domain); - if (appDomain.type === 'primary') { - writeAppNginxConfig(app, appDomain.fqdn, bundle, iteratorDone); - } else if (appDomain.type === 'alternate') { - writeAppRedirectNginxConfig(app, appDomain.fqdn, bundle, iteratorDone); - } else if (appDomain.type === 'alias') { - writeAppNginxConfig(app, appDomain.fqdn, bundle, iteratorDone); - } - }); - }, callback); + if (appDomain.type === 'primary') { + await writeAppNginxConfig(app, appDomain.fqdn, bundle); + } else if (appDomain.type === 'alternate') { + await writeAppRedirectNginxConfig(app, appDomain.fqdn, bundle); + } else if (appDomain.type === 'alias') { + await writeAppNginxConfig(app, appDomain.fqdn, bundle); + } + } } -function configureApp(app, auditSource, callback) { +async function configureApp(app, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); let appDomains = []; appDomains.push({ domain: app.domain, fqdn: app.fqdn, type: 'primary' }); @@ -629,31 +595,32 @@ function configureApp(app, auditSource, callback) { appDomains.push({ domain: aliasDomain.domain, fqdn: aliasDomain.fqdn, type: 'alias' }); }); - async.eachSeries(appDomains, function (appDomain, iteratorDone) { - ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle) { - if (error) return iteratorDone(error); + for (const appDomain of appDomains) { + const { bundle } = await ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource); - if (appDomain.type === 'primary') { - writeAppNginxConfig(app, appDomain.fqdn, bundle, iteratorDone); - } else if (appDomain.type === 'alternate') { - writeAppRedirectNginxConfig(app, appDomain.fqdn, bundle, iteratorDone); - } else if (appDomain.type === 'alias') { - writeAppNginxConfig(app, appDomain.fqdn, bundle, iteratorDone); - } - }); - }, callback); + if (appDomain.type === 'primary') { + await writeAppNginxConfig(app, appDomain.fqdn, bundle); + } else if (appDomain.type === 'alternate') { + await writeAppRedirectNginxConfig(app, appDomain.fqdn, bundle); + } else if (appDomain.type === 'alias') { + await writeAppNginxConfig(app, appDomain.fqdn, bundle); + } + } } -function unconfigureApp(app, callback) { +async function unconfigureApp(app) { assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof callback, 'function'); - // we use globbing to find all nginx configs for an app - rimraf(path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}*.conf`), function (error) { - if (error) debug('Error removing nginx configurations of "%s":', app.fqdn, error); + const configFilenames = safe.fs.readdirSync(paths.NGINX_APPCONFIG_DIR); + if (!configFilenames) throw new BoxError(BoxError.FS_ERROR, `Error loading nginx config files: ${safe.error.message}`); - reload(callback); - }); + for (const filename of configFilenames) { + if (!filename.startsWith(app.id)) continue; + + safe.fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, filename)); + } + + await reload(); } function renewCerts(options, auditSource, progressCallback, callback) { @@ -695,36 +662,34 @@ function renewCerts(options, auditSource, progressCallback, callback) { let progress = 1, renewed = []; - async.eachSeries(appDomains, function (appDomain, iteratorCallback) { + async.eachSeries(appDomains, async function (appDomain) { progressCallback({ percent: progress, message: `Ensuring certs of ${appDomain.fqdn}` }); progress += Math.round(100/appDomains.length); - ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle, state) { - if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet + const { bundle, renewed } = ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource); - if (state.renewed) renewed.push(appDomain.fqdn); + if (renewed) renewed.push(appDomain.fqdn); - if (appDomain.type === 'mail') return iteratorCallback(); // mail has no nginx config to check current cert + if (appDomain.type === 'mail') return; // mail has no nginx config to check current cert - // hack to check if the app's cert changed or not. this doesn't handle prod/staging le change since they use same file name - let currentNginxConfig = safe.fs.readFileSync(appDomain.nginxConfigFilename, 'utf8') || ''; - if (currentNginxConfig.includes(bundle.certFilePath)) return iteratorCallback(); + // hack to check if the app's cert changed or not. this doesn't handle prod/staging le change since they use same file name + let currentNginxConfig = safe.fs.readFileSync(appDomain.nginxConfigFilename, 'utf8') || ''; + if (currentNginxConfig.includes(bundle.certFilePath)) return; - debug(`renewCerts: creating new nginx config since ${appDomain.nginxConfigFilename} does not have ${bundle.certFilePath}`); + debug(`renewCerts: creating new nginx config since ${appDomain.nginxConfigFilename} does not have ${bundle.certFilePath}`); - // reconfigure since the cert changed - if (appDomain.type === 'webadmin' || appDomain.type === 'webadmin+mail') { - return writeDashboardNginxConfig(bundle, `${settings.dashboardFqdn()}.conf`, settings.dashboardFqdn(), iteratorCallback); - } else if (appDomain.type === 'primary') { - return writeAppNginxConfig(appDomain.app, appDomain.fqdn, bundle, iteratorCallback); - } else if (appDomain.type === 'alternate') { - return writeAppRedirectNginxConfig(appDomain.app, appDomain.fqdn, bundle, iteratorCallback); - } else if (appDomain.type === 'alias') { - return writeAppNginxConfig(appDomain.app, appDomain.fqdn, bundle, iteratorCallback); - } - - iteratorCallback(new BoxError(BoxError.INTERNAL_ERROR, `Unknown domain type for ${appDomain.fqdn}. This should never happen`)); - }); + // reconfigure since the cert changed + if (appDomain.type === 'webadmin' || appDomain.type === 'webadmin+mail') { + await writeDashboardNginxConfig(bundle, `${settings.dashboardFqdn()}.conf`, settings.dashboardFqdn()); + } else if (appDomain.type === 'primary') { + await writeAppNginxConfig(appDomain.app, appDomain.fqdn, bundle); + } else if (appDomain.type === 'alternate') { + await writeAppRedirectNginxConfig(appDomain.app, appDomain.fqdn, bundle); + } else if (appDomain.type === 'alias') { + await writeAppNginxConfig(appDomain.app, appDomain.fqdn, bundle); + } else { + throw new BoxError(BoxError.INTERNAL_ERROR, `Unknown domain type for ${appDomain.fqdn}. This should never happen`); + } }, function (error) { if (error) return callback(error); @@ -793,9 +758,8 @@ function removeAppConfigs() { } } -function writeDefaultConfig(options, callback) { +async function writeDefaultConfig(options) { assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); const certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert'); const keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key'); @@ -807,7 +771,7 @@ function writeDefaultConfig(options, callback) { // the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176) if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=${cn} -nodes`)) { debug(`writeDefaultConfig: could not generate certificate: ${safe.error.message}`); - return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error)); + throw new BoxError(BoxError.OPENSSL_ERROR, safe.error); } } @@ -827,7 +791,7 @@ function writeDefaultConfig(options, callback) { debug(`writeDefaultConfig: writing configs for endpoint "${data.endpoint}"`); - if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(new BoxError(BoxError.FS_ERROR, safe.error)); + if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) throw new BoxError(BoxError.FS_ERROR, safe.error); - reload(callback); + await reload(); } diff --git a/src/routes/apps.js b/src/routes/apps.js index 8612c6b4e..f4bf22f94 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -267,7 +267,7 @@ function setAutomaticUpdate(req, res, next) { }); } -function setReverseProxyConfig(req, res, next) { +async function setReverseProxyConfig(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.resource, 'object'); @@ -275,14 +275,13 @@ function setReverseProxyConfig(req, res, next) { if (req.body.csp !== null && typeof req.body.csp !== 'string') return next(new HttpError(400, 'csp is not a string')); - apps.setReverseProxyConfig(req.resource, req.body, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(apps.setReverseProxyConfig(req.resource, req.body, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, {})); - }); + next(new HttpSuccess(200, {})); } -function setCertificate(req, res, next) { +async function setCertificate(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.resource, 'object'); @@ -295,11 +294,10 @@ function setCertificate(req, res, next) { if (req.body.cert && !req.body.key) return next(new HttpError(400, 'key must be provided')); if (!req.body.cert && req.body.key) return next(new HttpError(400, 'cert must be provided')); - apps.setCertificate(req.resource, req.body, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(apps.setCertificate(req.resource, req.body, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, {})); - }); + next(new HttpSuccess(200, {})); } function setEnvironment(req, res, next) { diff --git a/src/test/reverseproxy-test.js b/src/test/reverseproxy-test.js index 47ee349ab..22db228f3 100644 --- a/src/test/reverseproxy-test.js +++ b/src/test/reverseproxy-test.js @@ -121,15 +121,15 @@ describe('Reverse Proxy', function () { }); }); - describe('generateFallbackCertificiate', function () { + describe('generateFallbackCertificate', function () { let domainObject = { domain: 'cool.com', config: {} }; let result; - it('can generate fallback certs', function () { - result = reverseProxy.generateFallbackCertificateSync(domainObject.domain); + it('can generate fallback certs', async function () { + result = await reverseProxy.generateFallbackCertificate(domainObject.domain); expect(result).to.be.ok(); expect(result.error).to.be(null); }); @@ -175,20 +175,14 @@ describe('Reverse Proxy', function () { await domains.update(domainCopy.domain, domainCopy, auditSource); }); - it('configure nginx correctly', function (done) { - reverseProxy.configureApp(app, auditSource, function (error) { - expect(fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + app.id + '.conf')); - expect(error).to.be(null); - done(); - }); + it('configure nginx correctly', async function () { + await reverseProxy.configureApp(app, auditSource); + expect(fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + app.id + '.conf')); }); - it('unconfigure nginx', function (done) { - reverseProxy.unconfigureApp(app, function (error) { - expect(!fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + app.id + '.conf')); - expect(error).to.be(null); - done(); - }); + it('unconfigure nginx', async function () { + await reverseProxy.unconfigureApp(app); + expect(!fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + app.id + '.conf')); }); }); });