diff --git a/src/certificates.js b/src/certificates.js index 59468c0b1..c11fbd491 100644 --- a/src/certificates.js +++ b/src/certificates.js @@ -5,6 +5,7 @@ exports = module.exports = { ensureFallbackCertificate: ensureFallbackCertificate, setFallbackCertificate: setFallbackCertificate, + getFallbackCertificate: getFallbackCertificate, validateCertificate: validateCertificate, ensureCertificate: ensureCertificate, @@ -276,7 +277,7 @@ function validateCertificate(cert, key, fqdn) { if (cert && !key) return new Error('missing key'); var result = safe.child_process.execSync('openssl x509 -noout -checkhost "' + fqdn + '"', { encoding: 'utf8', input: cert }); - if (!result) return new Error(util.format('could not get cert subject')); + if (!result) return new Error('Invalid certificate. Unable to get certificate subject.'); // if no match, check alt names if (result.indexOf('does match certificate') === -1) { @@ -289,17 +290,17 @@ function validateCertificate(cert, key, fqdn) { debug('validateCertificate: detected altNames as %j', altNames); // check altNames - if (!altNames.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, altNames)); + if (!altNames.some(matchesDomain)) return new Error(util.format('Certificate is not valid for this domain. Expecting %s in %j', fqdn, altNames)); } // http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert }); var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key }); - if (certModulus !== keyModulus) return new Error('key does not match the cert'); + if (certModulus !== keyModulus) return new Error('Key does not match the certificate.'); // check expiration result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert }); - if (!result) return new Error('cert expired'); + if (!result) return new Error('Certificate is expired.'); return null; } @@ -314,12 +315,12 @@ function setFallbackCertificate(cert, key, fqdn, callback) { if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message)); // backup the cert - if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); - if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); + if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); + if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); // copy over fallback cert - if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); - if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); + if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); + if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); exports.events.emit(exports.EVENT_CERT_CHANGED, '*.' + fqdn); @@ -330,11 +331,16 @@ function setFallbackCertificate(cert, key, fqdn, callback) { }); } -function getFallbackCertificatePath(callback) { +function getFallbackCertificate(fqdn, callback) { + assert.strictEqual(typeof fqdn, 'string'); assert.strictEqual(typeof callback, 'function'); - // any user fallback cert is always copied over to nginx cert dir - callback(null, path.join(paths.NGINX_CERT_DIR, 'host.cert'), path.join(paths.NGINX_CERT_DIR, 'host.key')); + var cert = safe.fs.readFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.cert'), 'utf-8'); + var key = safe.fs.readFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.key'), 'utf-8'); + + if (!cert || !key) return callback(new CertificatesError(CertificatesError.NOT_FOUND)); + + callback(null, { cert: cert, key: key }); } function setAdminCertificate(cert, key, callback) { @@ -372,7 +378,8 @@ function getAdminCertificatePath(callback) { if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath); - getFallbackCertificatePath(callback); + // any user fallback cert is always copied over to nginx cert dir + callback(null, path.join(paths.NGINX_CERT_DIR, 'host.cert'), path.join(paths.NGINX_CERT_DIR, 'host.key')); } function getAdminCertificate(callback) { @@ -425,7 +432,15 @@ function ensureCertificate(app, callback) { api.getCertificate(domain, apiOptions, function (error, certFilePath, keyFilePath) { if (error) { debug('ensureCertificate: could not get certificate. using fallback certs', error); - return callback(null, 'cert/host.cert', 'cert/host.key'); // use fallback certs + + var fallbackCertFilePath = path.join(paths.NGINX_CERT_DIR, app.domain + '.cert'); + var fallbackKeyFilePath = path.join(paths.NGINX_CERT_DIR, app.domain + '.key'); + + if (fs.existsSync(fallbackCertFilePath) && fs.existsSync(fallbackKeyFilePath)) { + return callback(null, fallbackCertFilePath, fallbackKeyFilePath); + } else { + return callback(null, 'cert/host.cert', 'cert/host.key'); + } } callback(null, certFilePath, keyFilePath); diff --git a/src/domains.js b/src/domains.js index ca2009a87..a8e6c594b 100644 --- a/src/domains.js +++ b/src/domains.js @@ -18,6 +18,7 @@ module.exports = exports = { var assert = require('assert'), certificates = require('./certificates.js'), + CertificatesError = certificates.CertificatesError, DatabaseError = require('./databaseerror.js'), debug = require('debug')('box:domains.js'), domaindb = require('./domaindb.js'), @@ -137,7 +138,13 @@ function get(domain, callback) { if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND)); if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error)); - return callback(null, result); + certificates.getFallbackCertificate(domain, function (error, fallbackCertificate) { + if (error && error.reason !== CertificatesError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error)); + + if (fallbackCertificate) result.fallbackCertificate = fallbackCertificate; + + return callback(null, result); + }); }); } diff --git a/src/routes/domains.js b/src/routes/domains.js index 2547e535e..48b4ba281 100644 --- a/src/routes/domains.js +++ b/src/routes/domains.js @@ -59,8 +59,8 @@ function update(req, res, next) { if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object')); if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings')); - if (req.body.fallbackCertificate && (!req.body.cert || typeof req.body.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string')); - if (req.body.fallbackCertificate && (!req.body.key || typeof req.body.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string')); + if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.cert || typeof req.body.fallbackCertificate.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string')); + if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.key || typeof req.body.fallbackCertificate.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string')); domains.update(req.params.domain, req.body.config, req.body.fallbackCertificate || null, function (error) { if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message)); diff --git a/webadmin/src/js/client.js b/webadmin/src/js/client.js index 71dacfba6..6bea0368a 100644 --- a/webadmin/src/js/client.js +++ b/webadmin/src/js/client.js @@ -1163,23 +1163,27 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification', }).error(defaultErrorHandler(callback)); }; - Client.prototype.addDomain = function (domain, config, callback) { + Client.prototype.addDomain = function (domain, config, fallbackCertificate, callback) { var data = { domain: domain, config: config }; - post('/api/v1/domains', data).success(function (data, status) { - if (status !== 201 || typeof data !== 'object') return callback(new ClientError(status, data)); - callback(null, data); - }).error(defaultErrorHandler(callback)); - }; + if (fallbackCertificate) data.fallbackCertificate = fallbackCertificate; - Client.prototype.updateDomain = function (domain, config, callback) { + post('/api/v1/domains', data).success(function (data, status) { + if (status !== 201 || typeof data !== 'object') return callback(new ClientError(status, data)); + callback(null, data); + }).error(defaultErrorHandler(callback)); + }; + + Client.prototype.updateDomain = function (domain, config, fallbackCertificate, callback) { var data = { - config: config + config: config }; + if (fallbackCertificate) data.fallbackCertificate = fallbackCertificate; + put('/api/v1/domains/' + domain, data).success(function (data, status) { if (status !== 204) return callback(new ClientError(status, data)); callback(null); diff --git a/webadmin/src/views/domains.html b/webadmin/src/views/domains.html index 6a1bad0cf..af956ca35 100644 --- a/webadmin/src/views/domains.html +++ b/webadmin/src/views/domains.html @@ -57,33 +57,60 @@ +
+ This domain must be hosted on AWS Route53. +
+ ++ This domain must be hosted on Google Cloud DNS. +
+ ++ This domain must be hosted on DigitalOcean. +
+ ++ This domain must be hosted on Cloudflare. +
+ ++ Setup A records for *.{{ domainConfigure.newDomain || domainConfigure.domain.domain }} and {{ domainConfigure.newDomain || domainConfigure.domain.domain }} to this server's IP. +
+ ++ All DNS records have to be setup manually before each app installation. +
+ ++ Certificates are automatically obtained and renewed from Let’s Encrypt. See the current rate limit here. + If provided, this wildcard certificate will be used for apps, should getting a Let’s Encrypt certificate fail. +
+- This domain must be hosted on AWS Route53. -
- -- This domain must be hosted on Google Cloud DNS. -
- -- This domain must be hosted on DigitalOcean. -
- -- This domain must be hosted on Cloudflare. -
- -- Setup A records for *.{{ domainConfigure.newDomain || domainConfigure.domain.domain }} and {{ domainConfigure.newDomain || domainConfigure.domain.domain }} to this server's IP. -
- -- All DNS records have to be setup manually before each app installation. -