diff --git a/migrations/20210504230054-domains-add-fallbackCertificateJson.js b/migrations/20210504230054-domains-add-fallbackCertificateJson.js index 5627ca149..eeb9077b5 100644 --- a/migrations/20210504230054-domains-add-fallbackCertificateJson.js +++ b/migrations/20210504230054-domains-add-fallbackCertificateJson.js @@ -6,7 +6,7 @@ const async = require('async'), const CERTS_DIR = '/home/yellowtent/boxdata/certs'; exports.up = function(db, callback) { - db.runSql('ALTER TABLE domains ADD COLUMN fallbackCertificateJson TEXT', function (error) { + db.runSql('ALTER TABLE domains ADD COLUMN fallbackCertificateJson MEDIUMTEXT', function (error) { if (error) return callback(error); db.all('SELECT * FROM domains', [ ], function (error, domains) { diff --git a/migrations/20210505165936-subdomains-add-certificateJson.js b/migrations/20210505165936-subdomains-add-certificateJson.js new file mode 100644 index 000000000..62cd58113 --- /dev/null +++ b/migrations/20210505165936-subdomains-add-certificateJson.js @@ -0,0 +1,30 @@ +'use strict'; + +const async = require('async'), + fs = require('fs'); + +const CERTS_DIR = '/home/yellowtent/boxdata/certs'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE subdomains ADD COLUMN certificateJson MEDIUMTEXT', function (error) { + if (error) return callback(error); + + db.all('SELECT * FROM subdomains', [ ], function (error, subdomains) { + if (error) return callback(error); + + async.eachSeries(subdomains, function (subdomain, iteratorDone) { + const cert = fs.readFileSync(`${CERTS_DIR}/${subdomain.subdomain}.${subdomain.domain}.user.cert`, 'utf8'); + const key = fs.readFileSync(`${CERTS_DIR}/${subdomain.subdomain}.${subdomain.domain}.user.key`, 'utf8'); + const certificate = { cert, key }; + + db.runSql('UPDATE subdomains SET certificateJson=? WHERE domain=? AND subdomain=?', [ JSON.stringify(certificate), subdomain.domain, subdomain.subdomain ], iteratorDone); + }, callback); + }); + }); +}; + +exports.down = function(db, callback) { + async.series([ + db.runSql.run(db, 'ALTER TABLE subdomains DROP COLUMN certificateJson') + ], callback); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index 637f67467..d7fa8fbe6 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -157,7 +157,7 @@ CREATE TABLE IF NOT EXISTS domains( tlsConfigJson TEXT, /* JSON containing the tls provider config */ wellKnownJson TEXT, /* JSON containing well known docs for this domain */ - fallbackCertificateJson TEXT, + fallbackCertificateJson MEDIUMTEXT, PRIMARY KEY (domain)) @@ -210,6 +210,8 @@ CREATE TABLE IF NOT EXISTS subdomains( subdomain VARCHAR(128) NOT NULL, type VARCHAR(128) NOT NULL, /* primary or redirect */ + certificateJson MEDIUMTEXT, + FOREIGN KEY(domain) REFERENCES domains(domain), FOREIGN KEY(appId) REFERENCES apps(id), UNIQUE (subdomain, domain)); diff --git a/src/apps.js b/src/apps.js index a4de33875..0b83dd066 100644 --- a/src/apps.js +++ b/src/apps.js @@ -728,8 +728,6 @@ function install(data, auditSource, callback) { portBindings = data.portBindings || null, accessRestriction = data.accessRestriction || null, icon = data.icon || null, - cert = data.cert || null, - key = data.key || null, memoryLimit = data.memoryLimit || 0, sso = 'sso' in data ? data.sso : null, debugMode = data.debugMode || null, @@ -794,11 +792,6 @@ function install(data, auditSource, callback) { validateLocations(locations, function (error, domainObjectMap) { if (error) return callback(error); - if (cert && key) { - error = reverseProxy.validateCertificate(location, domainObjectMap[domain], { cert, key }); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'cert' })); - } - debug('Will install app with id : ' + appId); var data = { @@ -827,12 +820,6 @@ function install(data, auditSource, callback) { purchaseApp({ appId: appId, appstoreId: appStoreId, manifestId: manifest.id || 'customapp' }, function (error) { if (error) return callback(error); - // save cert to boxdata/certs - if (cert && key) { - let error = reverseProxy.setAppCertificateSync(location, domainObjectMap[domain], { cert, key }); - if (error) return callback(error); - } - const task = { args: { restoreConfig: null, skipDnsSetup, overwriteDns }, values: { }, @@ -1164,28 +1151,30 @@ function setReverseProxyConfig(app, reverseProxyConfig, auditSource, callback) { }); } -function setCertificate(app, bundle, auditSource, callback) { +function setCertificate(app, data, auditSource, callback) { assert.strictEqual(typeof app, 'object'); - assert(bundle && typeof bundle === 'object'); + assert(data && typeof data === 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); const appId = app.id; + const { location, domain, cert, key } = data; - domains.get(app.domain, function (error, domainObject) { + domains.get(domain, function (error, domainObject) { if (error) return callback(error); - if (bundle.cert && bundle.key) { - error = reverseProxy.validateCertificate(app.location, domainObject, { cert: bundle.cert, key: bundle.key }); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'cert' })); + if (cert && key) { + error = reverseProxy.validateCertificate(location, domainObject, { cert, key }); + if (error) return callback(error); } - error = reverseProxy.setAppCertificateSync(app.location, domainObject, { cert: bundle.cert, key: bundle.key }); - if (error) return callback(error); + reverseProxy.setAppCertificateSync(location, domainObject, { cert, key }, function (error) { + if (error) return callback(error); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cert: bundle.cert, key: bundle.key }); + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cert, key }); - callback(); + callback(); + }); }); } diff --git a/src/docker.js b/src/docker.js index 6bca8e645..888906d34 100644 --- a/src/docker.js +++ b/src/docker.js @@ -244,7 +244,7 @@ function getAddonMounts(app, callback) { return iteratorDone(); case 'tls': - reverseProxy.getCertificate(app.fqdn, app.domain, function (error, bundle) { + reverseProxy.getCertificatePath(app.fqdn, app.domain, function (error, bundle) { if (error) return iteratorDone(error); mounts.push({ diff --git a/src/mail.js b/src/mail.js index 5f0d2de72..fb8f447bf 100644 --- a/src/mail.js +++ b/src/mail.js @@ -643,7 +643,7 @@ function configureMail(mailFqdn, mailDomain, serviceConfig, callback) { const memory = system.getMemoryAllocation(memoryLimit); const cloudronToken = hat(8 * 128), relayToken = hat(8 * 128); - reverseProxy.getCertificate(mailFqdn, mailDomain, function (error, bundle) { + reverseProxy.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 8fd8f45e8..6d9619680 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -1,14 +1,14 @@ 'use strict'; exports = module.exports = { + setAppCertificate, setFallbackCertificate, generateFallbackCertificateSync, - setAppCertificateSync, validateCertificate, - getCertificate, + getCertificatePath, ensureCertificate, renewCerts, @@ -239,11 +239,7 @@ function setFallbackCertificate(domain, fallback, callback) { if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); // TODO: maybe the cert is being used by the mail container - reload(function (error) { - if (error) return callback(new BoxError(BoxError.NGINX_ERROR, error)); - - return callback(null); - }); + reload(callback); } function restoreFallbackCertificates(callback) { @@ -270,10 +266,11 @@ function getFallbackCertificatePathSync(domain) { return { certFilePath, keyFilePath }; } -function setAppCertificateSync(location, domainObject, certificate) { +function setAppCertificate(location, domainObject, certificate, callback) { assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof certificate, 'object'); + assert.strictEqual(typeof callback, 'function'); let fqdn = domains.fqdn(location, domainObject); if (certificate.cert && certificate.key) { @@ -284,10 +281,10 @@ function setAppCertificateSync(location, domainObject, certificate) { if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`))) debug('Error removing key: ' + safe.error.message); } - return null; + reload(callback); } -function getAcmeCertificate(vhost, domainObject, callback) { +function getAcmeCertificatePath(vhost, domainObject, callback) { assert.strictEqual(typeof vhost, 'string'); // this can contain wildcard domain (for alias domains) assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof callback, 'function'); @@ -310,7 +307,7 @@ function getAcmeCertificate(vhost, domainObject, callback) { callback(null); } -function getCertificate(fqdn, domain, callback) { +function getCertificatePath(fqdn, domain, callback) { assert.strictEqual(typeof fqdn, 'string'); assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof callback, 'function'); @@ -330,7 +327,7 @@ function getCertificate(fqdn, domain, callback) { if (domainObject.tlsConfig.provider === 'fallback') return callback(null, getFallbackCertificatePathSync(domain)); - getAcmeCertificate(fqdn, domainObject, function (error, result) { + getAcmeCertificatePath(fqdn, domainObject, function (error, result) { if (error || result) return callback(error, result); return callback(null, getFallbackCertificatePathSync(domain)); @@ -365,7 +362,7 @@ function ensureCertificate(vhost, domain, auditSource, callback) { getAcmeApi(domainObject, function (error, acmeApi, apiOptions) { if (error) return callback(error); - getAcmeCertificate(vhost, domainObject, function (_error, currentBundle) { + getAcmeCertificatePath(vhost, domainObject, function (_error, currentBundle) { if (currentBundle) { debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`); @@ -453,7 +450,7 @@ function writeDashboardConfig(domain, callback) { const adminFqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject); - getCertificate(adminFqdn, domainObject.domain, function (error, bundle) { + getCertificatePath(adminFqdn, domainObject.domain, function (error, bundle) { if (error) return callback(error); writeDashboardNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback); @@ -564,7 +561,7 @@ function writeAppConfig(app, callback) { }); async.eachSeries(appDomains, function (appDomain, iteratorDone) { - getCertificate(appDomain.fqdn, appDomain.domain, function (error, bundle) { + getCertificatePath(appDomain.fqdn, appDomain.domain, function (error, bundle) { if (error) return iteratorDone(error); if (appDomain.type === 'primary') { diff --git a/src/routes/apps.js b/src/routes/apps.js index 14b9e13eb..e6d55ee64 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -57,7 +57,6 @@ var apps = require('../apps.js'), HttpSuccess = require('connect-lastmile').HttpSuccess, safe = require('safetydance'), users = require('../users.js'), - util = require('util'), WebSocket = require('ws'); function load(req, res, next) { @@ -121,12 +120,6 @@ function install(req, res, next) { if ('label' in data && typeof data.label !== 'string') return next(new HttpError(400, 'label must be a string')); - // falsy values in cert and key unset the cert - if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string')); - if (data.cert && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string')); - if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided')); - if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided')); - if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number')); if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean')); @@ -292,6 +285,10 @@ function setCertificate(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.resource, 'object'); + if (typeof req.body.location !== 'string') return next(new HttpError(400, 'location must be string')); // location may be an empty string + if (!req.body.domain) return next(new HttpError(400, 'domain is required')); + if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be string')); + if (req.body.key !== null && typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string')); if (req.body.cert !== null && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string')); if (req.body.cert && !req.body.key) return next(new HttpError(400, 'key must be provided'));