diff --git a/src/apps.js b/src/apps.js index 4a213db26..d524ab325 100644 --- a/src/apps.js +++ b/src/apps.js @@ -563,21 +563,36 @@ async function getStorageDir(app) { return path.join(volume.hostPath, app.storageVolumePrefix); } +function removeCertificateKeys(app) { + if (app.certificate) delete app.certificate.key; + app.secondaryDomains.forEach(sd => { if (sd.certificate) delete sd.certificate.key; }); + app.aliasDomains.forEach(ad => { if (ad.certificate) delete ad.certificate.key; }); + app.redirectDomains.forEach(rd => { if (rd.certificate) delete rd.certificate.key; }); +} + function removeInternalFields(app) { - return _.pick(app, + const result = _.pick(app, 'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', - 'subdomain', 'domain', 'fqdn', 'crontab', 'upstreamUri', + 'subdomain', 'domain', 'fqdn', 'certificate', 'crontab', 'upstreamUri', 'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares', 'operators', 'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags', - 'label', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'storageVolumeId', 'storageVolumePrefix', 'mounts', + 'label', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', + 'storageVolumeId', 'storageVolumePrefix', 'mounts', 'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain'); + + removeCertificateKeys(result); + return result; } // non-admins can only see these function removeRestrictedFields(app) { - return _.pick(app, - 'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'sso', - 'subdomain', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup', 'upstreamUri'); + const result = _.pick(app, + 'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction', + 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'sso', 'subdomain', 'domain', 'fqdn', 'certificate', + 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup', 'upstreamUri'); + + removeCertificateKeys(result); + return result; } async function getIcon(app, options) { @@ -675,30 +690,35 @@ function postProcess(result) { const subdomains = JSON.parse(result.subdomains), domains = JSON.parse(result.domains), subdomainTypes = JSON.parse(result.subdomainTypes), - subdomainEnvironmentVariables = JSON.parse(result.subdomainEnvironmentVariables); + subdomainEnvironmentVariables = JSON.parse(result.subdomainEnvironmentVariables), + subdomainCertificateJsons = JSON.parse(result.subdomainCertificateJsons); delete result.subdomains; delete result.domains; delete result.subdomainTypes; delete result.subdomainEnvironmentVariables; + delete result.subdomainCertificateJsons; result.secondaryDomains = []; result.redirectDomains = []; result.aliasDomains = []; for (let i = 0; i < subdomainTypes.length; i++) { + const subdomain = subdomains[i], domain = domains[i], certificate = safe.JSON.parse(subdomainCertificateJsons[i]); + if (subdomainTypes[i] === exports.LOCATION_TYPE_PRIMARY) { - result.subdomain = subdomains[i]; - result.domain = domains[i]; + result.subdomain = subdomain; + result.domain = domain; + result.certificate = certificate; } else if (subdomainTypes[i] === exports.LOCATION_TYPE_SECONDARY) { - result.secondaryDomains.push({ domain: domains[i], subdomain: subdomains[i], environmentVariable: subdomainEnvironmentVariables[i] }); + result.secondaryDomains.push({ domain, subdomain, certificate, environmentVariable: subdomainEnvironmentVariables[i] }); } else if (subdomainTypes[i] === exports.LOCATION_TYPE_REDIRECT) { - result.redirectDomains.push({ domain: domains[i], subdomain: subdomains[i] }); + result.redirectDomains.push({ domain, subdomain, certificate }); } else if (subdomainTypes[i] === exports.LOCATION_TYPE_ALIAS) { - result.aliasDomains.push({ domain: domains[i], subdomain: subdomains[i] }); + result.aliasDomains.push({ domain, subdomain, certificate }); } } - let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues); + const envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues); delete result.envNames; delete result.envValues; result.env = {}; @@ -706,7 +726,7 @@ function postProcess(result) { if (envNames[i]) result.env[envNames[i]] = envValues[i]; } - let volumeIds = JSON.parse(result.volumeIds); + const volumeIds = JSON.parse(result.volumeIds); delete result.volumeIds; let volumeReadOnlys = JSON.parse(result.volumeReadOnlys); delete result.volumeReadOnlys; @@ -1038,9 +1058,9 @@ async function getDomainObjectMap() { // each query simply join apps table with another table by id. we then join the full result together const PB_QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id'; const ENV_QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id'; -const SUBDOMAIN_QUERY = 'SELECT id, JSON_ARRAYAGG(locations.subdomain) AS subdomains, JSON_ARRAYAGG(locations.domain) AS domains, JSON_ARRAYAGG(locations.type) AS subdomainTypes, JSON_ARRAYAGG(locations.environmentVariable) AS subdomainEnvironmentVariables FROM apps LEFT JOIN locations ON apps.id = locations.appId GROUP BY apps.id'; +const SUBDOMAIN_QUERY = 'SELECT id, JSON_ARRAYAGG(locations.subdomain) AS subdomains, JSON_ARRAYAGG(locations.domain) AS domains, JSON_ARRAYAGG(locations.type) AS subdomainTypes, JSON_ARRAYAGG(locations.environmentVariable) AS subdomainEnvironmentVariables, JSON_ARRAYAGG(locations.certificateJson) AS subdomainCertificateJsons FROM apps LEFT JOIN locations ON apps.id = locations.appId GROUP BY apps.id'; const MOUNTS_QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id'; -const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, subdomains, domains, subdomainTypes, subdomainEnvironmentVariables, volumeIds, volumeReadOnlys FROM apps` +const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, subdomains, domains, subdomainTypes, subdomainEnvironmentVariables, subdomainCertificateJsons, volumeIds, volumeReadOnlys FROM apps` + ` LEFT JOIN (${PB_QUERY}) AS q1 on q1.id = apps.id` + ` LEFT JOIN (${ENV_QUERY}) AS q2 on q2.id = apps.id` + ` LEFT JOIN (${SUBDOMAIN_QUERY}) AS q3 on q3.id = apps.id` @@ -1749,17 +1769,23 @@ async function setCertificate(app, data, auditSource) { assert(data && typeof data === 'object'); assert.strictEqual(typeof auditSource, 'object'); - const { location, domain, cert, key } = data; + const { subdomain, domain, cert, key } = data; const domainObject = await domains.get(domain); + if (domainObject === null) throw new BoxError(BoxError.NOT_FOUND, 'Domain not found'); if (cert && key) { - const error = reverseProxy.validateCertificate(location, domainObject, { cert, key }); + const error = reverseProxy.validateCertificate(subdomain, domainObject, { cert, key }); if (error) throw error; } - await reverseProxy.setAppCertificate(location, domainObject, { cert, key }); - await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: app.id, app, cert, key }); + const certificateJson = cert && key ? JSON.stringify({ cert, key }) : null; + const result = await database.query('UPDATE locations SET certificateJson=? WHERE location=? AND domain=?', [ certificateJson, subdomain, domain ]); + if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Location not found'); + + app = await get(app.id); // refresh app object + await reverseProxy.writeAppConfigs(app); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: app.id, app, subdomain, domain, cert }); } async function setLocation(app, data, auditSource) { diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 8de6297ab..22f44c754 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -276,17 +276,17 @@ function getAcmeCertificatePathSync(fqdn, domainObject) { return { certName, certFilePath, keyFilePath, csrFilePath, acmeChallengesDir }; } -async function setAppCertificate(subdomain, domainObject, bundle) { +async function setAppCertificate(subdomain, domainObject, certificate) { assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof domainObject, 'object'); - assert(bundle && typeof bundle === 'object'); + assert.strictEqual(typeof certificate, 'object'); const fqdn = dns.fqdn(subdomain, domainObject); const { certFilePath, keyFilePath } = getAppCertificatePathSync(fqdn); - if (bundle.cert && bundle.key) { - if (!safe.fs.writeFileSync(certFilePath, bundle.cert)) throw safe.error; - if (!safe.fs.writeFileSync(keyFilePath, bundle.key)) throw safe.error; + if (certificate !== null) { + 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}`);