2015-12-10 13:31:47 -08:00
|
|
|
'use strict';
|
|
|
|
|
|
2016-05-06 18:44:37 +02:00
|
|
|
exports = module.exports = {
|
2021-05-05 10:34:22 -07:00
|
|
|
setAppCertificate,
|
2020-08-13 14:00:55 -07:00
|
|
|
setFallbackCertificate,
|
2017-01-17 09:57:15 -08:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
generateFallbackCertificate,
|
2018-11-05 19:09:58 -08:00
|
|
|
|
2020-08-13 14:00:55 -07:00
|
|
|
validateCertificate,
|
2017-01-17 09:57:15 -08:00
|
|
|
|
2021-05-05 10:34:22 -07:00
|
|
|
getCertificatePath,
|
2020-08-13 14:00:55 -07:00
|
|
|
ensureCertificate,
|
2016-06-22 13:48:07 -05:00
|
|
|
|
2021-05-18 13:28:48 -07:00
|
|
|
checkCerts,
|
2017-01-17 09:57:15 -08:00
|
|
|
|
2019-09-30 15:25:53 -07:00
|
|
|
// the 'configure' ensure a certificate and generate nginx config
|
2020-08-13 14:00:55 -07:00
|
|
|
configureApp,
|
|
|
|
|
unconfigureApp,
|
2018-01-30 13:54:13 -08:00
|
|
|
|
2019-09-30 15:25:53 -07:00
|
|
|
// these only generate nginx config
|
2020-08-13 14:00:55 -07:00
|
|
|
writeDefaultConfig,
|
2020-09-23 15:45:04 -07:00
|
|
|
writeDashboardConfig,
|
2020-08-13 14:00:55 -07:00
|
|
|
writeAppConfig,
|
|
|
|
|
|
|
|
|
|
removeAppConfigs,
|
2021-05-04 21:40:11 -07:00
|
|
|
restoreFallbackCertificates,
|
2018-01-30 12:23:27 -08:00
|
|
|
|
2016-06-22 13:48:07 -05:00
|
|
|
// exported for testing
|
2020-08-07 22:59:57 -07:00
|
|
|
_getAcmeApi: getAcmeApi
|
2016-05-06 18:44:37 +02:00
|
|
|
};
|
|
|
|
|
|
2021-05-07 22:44:13 -07:00
|
|
|
const acme2 = require('./acme2.js'),
|
2016-03-17 12:20:02 -07:00
|
|
|
apps = require('./apps.js'),
|
2015-12-10 13:31:47 -08:00
|
|
|
assert = require('assert'),
|
2021-05-02 23:28:41 -07:00
|
|
|
blobs = require('./blobs.js'),
|
2019-10-22 16:46:24 -07:00
|
|
|
BoxError = require('./boxerror.js'),
|
2015-12-11 13:52:21 -08:00
|
|
|
constants = require('./constants.js'),
|
2018-02-09 13:44:29 -08:00
|
|
|
crypto = require('crypto'),
|
2018-10-31 15:41:02 -07:00
|
|
|
debug = require('debug')('box:reverseproxy'),
|
2021-08-13 17:22:28 -07:00
|
|
|
dns = require('./dns.js'),
|
2018-01-31 18:27:18 +01:00
|
|
|
domains = require('./domains.js'),
|
2018-01-30 12:23:27 -08:00
|
|
|
ejs = require('ejs'),
|
2016-04-30 22:27:33 -07:00
|
|
|
eventlog = require('./eventlog.js'),
|
2015-12-11 13:52:21 -08:00
|
|
|
fs = require('fs'),
|
2019-03-04 15:20:58 -08:00
|
|
|
mail = require('./mail.js'),
|
2018-02-09 13:44:29 -08:00
|
|
|
os = require('os'),
|
2015-12-11 13:52:21 -08:00
|
|
|
path = require('path'),
|
2015-12-10 13:31:47 -08:00
|
|
|
paths = require('./paths.js'),
|
2015-12-10 20:31:38 -08:00
|
|
|
safe = require('safetydance'),
|
2019-07-26 10:49:29 -07:00
|
|
|
settings = require('./settings.js'),
|
2018-01-30 12:23:27 -08:00
|
|
|
shell = require('./shell.js'),
|
2019-07-25 11:26:53 -07:00
|
|
|
sysinfo = require('./sysinfo.js'),
|
2018-04-29 10:58:45 -07:00
|
|
|
users = require('./users.js'),
|
2021-11-16 22:56:35 -08:00
|
|
|
util = require('util');
|
2015-12-10 13:31:47 -08:00
|
|
|
|
2021-03-23 11:01:14 -07:00
|
|
|
const NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' });
|
|
|
|
|
const RESTART_SERVICE_CMD = path.join(__dirname, 'scripts/restartservice.sh');
|
2018-01-30 12:23:27 -08:00
|
|
|
|
2021-01-08 14:10:11 -08:00
|
|
|
function nginxLocation(s) {
|
|
|
|
|
if (!s.startsWith('!')) return s;
|
|
|
|
|
|
|
|
|
|
let re = s.replace(/[\^$\\.*+?()[\]{}|]/g, '\\$&'); // https://github.com/es-shims/regexp.escape/blob/master/implementation.js
|
|
|
|
|
|
|
|
|
|
return `~ ^(?!(${re.slice(1)}))`; // negative regex assertion - https://stackoverflow.com/questions/16302897/nginx-location-not-equal-to-regex
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-15 09:50:11 -07:00
|
|
|
async function getAcmeApi(domainObject) {
|
2018-11-14 19:36:12 -08:00
|
|
|
assert.strictEqual(typeof domainObject, 'object');
|
2016-04-19 08:21:23 -07:00
|
|
|
|
2021-07-15 09:50:11 -07:00
|
|
|
const acmeApi = acme2;
|
2016-12-05 17:01:23 +01:00
|
|
|
|
2021-07-15 09:50:11 -07:00
|
|
|
let apiOptions = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
|
|
|
|
|
apiOptions.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
|
|
|
|
|
apiOptions.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
|
|
|
|
|
apiOptions.wildcard = !!domainObject.tlsConfig.wildcard;
|
2016-01-13 14:21:23 -08:00
|
|
|
|
2018-11-14 19:36:12 -08:00
|
|
|
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
|
|
|
|
// we cannot use admin@fqdn because the user might not have set it up.
|
|
|
|
|
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
|
|
|
|
// https://github.com/ietf-wg-acme/acme/issues/30
|
2021-07-15 09:50:11 -07:00
|
|
|
const [error, owner] = await safe(users.getOwner());
|
2021-07-29 17:08:01 +02:00
|
|
|
apiOptions.email = (error || !owner) ? 'webmaster@cloudron.io' : owner.email; // can error if not activated yet
|
2015-12-17 13:17:46 -08:00
|
|
|
|
2021-07-15 09:50:11 -07:00
|
|
|
return { acmeApi, apiOptions };
|
2015-12-14 12:28:00 -08:00
|
|
|
}
|
|
|
|
|
|
2021-06-01 09:09:16 -07:00
|
|
|
function getExpiryDate(certFilePath) {
|
2016-03-23 08:49:08 -07:00
|
|
|
assert.strictEqual(typeof certFilePath, 'string');
|
2016-03-19 12:50:31 -07:00
|
|
|
|
2021-06-01 09:09:16 -07:00
|
|
|
if (!fs.existsSync(certFilePath)) return null; // not found
|
2015-12-14 12:38:19 -08:00
|
|
|
|
2021-06-04 17:51:26 -07:00
|
|
|
const result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-enddate', '-noout', '-in', certFilePath ]);
|
2021-06-01 09:09:16 -07:00
|
|
|
if (!result) return null; // some error
|
2016-03-18 22:59:51 -07:00
|
|
|
|
2021-06-01 09:09:16 -07:00
|
|
|
const notAfter = result.stdout.toString('utf8').trim().split('=')[1];
|
2021-06-24 00:48:54 -07:00
|
|
|
const notAfterDate = new Date(notAfter);
|
2018-11-23 11:26:18 -08:00
|
|
|
|
2021-06-24 00:48:54 -07:00
|
|
|
const daysLeft = (notAfterDate - new Date())/(24 * 60 * 60 * 1000);
|
|
|
|
|
debug(`expiryDate: ${certFilePath} notAfter=${notAfter} daysLeft=${daysLeft}`);
|
|
|
|
|
|
|
|
|
|
return notAfterDate;
|
2015-12-14 12:38:19 -08:00
|
|
|
}
|
|
|
|
|
|
2021-09-22 09:13:16 -07:00
|
|
|
async function isOcspEnabled(certFilePath) {
|
|
|
|
|
// on some servers, OCSP does not work. see #796
|
|
|
|
|
const config = await settings.getReverseProxyConfig();
|
|
|
|
|
if (!config.ocsp) return false;
|
|
|
|
|
|
|
|
|
|
// We used to check for the must-staple in the cert using openssl x509 -text -noout -in ${certFilePath} | grep -q status_request
|
|
|
|
|
// however, we cannot set the must-staple because first request to nginx fails because of it's OCSP caching behavior
|
2021-04-16 13:33:32 -07:00
|
|
|
const result = safe.child_process.execSync(`openssl x509 -in ${certFilePath} -noout -ocsp_uri`, { encoding: 'utf8' });
|
|
|
|
|
return result && result.length > 0; // no error and has uri
|
2021-04-16 11:17:13 -07:00
|
|
|
}
|
|
|
|
|
|
2018-11-14 19:46:38 -08:00
|
|
|
// checks if the certificate matches the options provided by user (like wildcard, le-staging etc)
|
|
|
|
|
function providerMatchesSync(domainObject, certFilePath, apiOptions) {
|
|
|
|
|
assert.strictEqual(typeof domainObject, 'object');
|
2018-10-24 19:52:07 -07:00
|
|
|
assert.strictEqual(typeof certFilePath, 'string');
|
|
|
|
|
assert.strictEqual(typeof apiOptions, 'object');
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(certFilePath)) return false; // not found
|
|
|
|
|
|
|
|
|
|
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
|
2018-11-23 11:39:00 -08:00
|
|
|
if (!subjectAndIssuer) return false; // something bad happenned
|
2018-10-24 19:52:07 -07:00
|
|
|
|
2018-11-15 14:18:34 +01:00
|
|
|
const subject = subjectAndIssuer.match(/^subject=(.*)$/m)[1];
|
2018-11-15 10:45:27 -08:00
|
|
|
const domain = subject.substr(subject.indexOf('=') + 1).trim(); // subject can be /CN=, CN=, CN = and other forms
|
2018-11-14 19:46:38 -08:00
|
|
|
const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1];
|
2018-11-15 10:45:27 -08:00
|
|
|
const isWildcardCert = domain.includes('*');
|
2021-04-27 12:55:11 -07:00
|
|
|
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt') && !issuer.includes('STAGING');
|
2018-11-14 19:46:38 -08:00
|
|
|
|
|
|
|
|
const issuerMismatch = (apiOptions.prod && !isLetsEncryptProd) || (!apiOptions.prod && isLetsEncryptProd);
|
|
|
|
|
// bare domain is not part of wildcard SAN
|
2018-11-15 10:45:27 -08:00
|
|
|
const wildcardMismatch = (domain !== domainObject.domain) && (apiOptions.wildcard && !isWildcardCert) || (!apiOptions.wildcard && isWildcardCert);
|
2018-10-24 19:52:07 -07:00
|
|
|
|
2018-11-14 19:46:38 -08:00
|
|
|
const mismatch = issuerMismatch || wildcardMismatch;
|
2018-10-24 19:52:07 -07:00
|
|
|
|
2020-12-04 11:47:19 -08:00
|
|
|
debug(`providerMatchesSync: ${certFilePath} subject=${subject} domain=${domain} issuer=${issuer} `
|
|
|
|
|
+ `wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} `
|
|
|
|
|
+ `issuerMismatch=${issuerMismatch} wildcardMismatch=${wildcardMismatch} match=${!mismatch}`);
|
2018-10-24 19:52:07 -07:00
|
|
|
|
|
|
|
|
return !mismatch;
|
|
|
|
|
}
|
|
|
|
|
|
2015-12-10 20:31:38 -08:00
|
|
|
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
|
|
|
|
|
// servers certificate appears first (and not the intermediate cert)
|
2018-11-05 22:36:16 -08:00
|
|
|
function validateCertificate(location, domainObject, certificate) {
|
2018-11-05 19:09:58 -08:00
|
|
|
assert.strictEqual(typeof location, 'string');
|
2018-11-05 21:26:53 -08:00
|
|
|
assert.strictEqual(typeof domainObject, 'object');
|
2018-11-05 22:36:16 -08:00
|
|
|
assert(certificate && typeof certificate, 'object');
|
|
|
|
|
|
|
|
|
|
const cert = certificate.cert, key = certificate.key;
|
2015-12-10 20:31:38 -08:00
|
|
|
|
2018-01-26 19:31:06 -08:00
|
|
|
// check for empty cert and key strings
|
2022-02-07 13:19:59 -08:00
|
|
|
if (!cert && key) return new BoxError(BoxError.BAD_FIELD, 'missing cert');
|
|
|
|
|
if (cert && !key) return new BoxError(BoxError.BAD_FIELD, 'missing key');
|
2015-12-10 20:31:38 -08:00
|
|
|
|
2018-02-09 14:05:01 -08:00
|
|
|
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
|
2021-08-13 17:22:28 -07:00
|
|
|
const fqdn = dns.fqdn(location, domainObject);
|
2018-11-05 19:09:58 -08:00
|
|
|
|
2020-03-24 20:56:49 -07:00
|
|
|
let result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
|
2022-02-07 13:19:59 -08:00
|
|
|
if (result === null) return new BoxError(BoxError.BAD_FIELD, 'Unable to get certificate subject:' + safe.error.message);
|
2017-05-11 21:55:25 +02:00
|
|
|
|
2022-02-07 13:19:59 -08:00
|
|
|
if (result.indexOf('does match certificate') === -1) return new BoxError(BoxError.BAD_FIELD, `Certificate is not valid for this domain. Expecting ${fqdn}`);
|
2015-12-10 20:31:38 -08:00
|
|
|
|
2020-03-24 20:56:49 -07:00
|
|
|
// check if public key in the cert and private key matches. pkey below works for RSA and ECDSA keys
|
|
|
|
|
const pubKeyFromCert = safe.child_process.execSync('openssl x509 -noout -pubkey', { encoding: 'utf8', input: cert });
|
2022-02-07 13:19:59 -08:00
|
|
|
if (pubKeyFromCert === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get public key from certificate: ${safe.error.message}`);
|
2018-11-23 11:39:00 -08:00
|
|
|
|
2020-03-24 20:56:49 -07:00
|
|
|
const pubKeyFromKey = safe.child_process.execSync('openssl pkey -pubout', { encoding: 'utf8', input: key });
|
2022-02-07 13:19:59 -08:00
|
|
|
if (pubKeyFromKey === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get public key from private key: ${safe.error.message}`);
|
2018-11-23 11:39:00 -08:00
|
|
|
|
2022-02-07 13:19:59 -08:00
|
|
|
if (pubKeyFromCert !== pubKeyFromKey) return new BoxError(BoxError.BAD_FIELD, 'Public key does not match the certificate.');
|
2015-12-10 20:31:38 -08:00
|
|
|
|
2017-11-27 10:39:42 -08:00
|
|
|
// check expiration
|
2017-02-24 19:21:53 -08:00
|
|
|
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
|
2022-02-07 13:19:59 -08:00
|
|
|
if (!result) return new BoxError(BoxError.BAD_FIELD, 'Certificate has expired.');
|
2017-02-24 19:21:53 -08:00
|
|
|
|
2015-12-10 20:31:38 -08:00
|
|
|
return null;
|
|
|
|
|
}
|
2015-12-11 13:52:21 -08:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
async function reload() {
|
|
|
|
|
if (constants.TEST) return;
|
2018-01-30 16:14:05 -08:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
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}`);
|
2018-01-30 16:14:05 -08:00
|
|
|
}
|
|
|
|
|
|
2021-10-06 13:16:36 -07:00
|
|
|
// this is used in migration - 20211006200150-domains-ensure-fallbackCertificate.js
|
2021-08-17 14:04:29 -07:00
|
|
|
async function generateFallbackCertificate(domain) {
|
2021-05-04 21:40:11 -07:00
|
|
|
assert.strictEqual(typeof domain, 'string');
|
2018-11-05 19:09:58 -08:00
|
|
|
|
|
|
|
|
const certFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.cert`);
|
|
|
|
|
const keyFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.key`);
|
|
|
|
|
|
2021-10-06 13:16:36 -07:00
|
|
|
const opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
|
2018-11-05 19:09:58 -08:00
|
|
|
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
|
|
|
|
|
let opensslConfWithSan;
|
2021-10-06 13:16:36 -07:00
|
|
|
const cn = domain;
|
2018-11-05 20:36:58 -08:00
|
|
|
|
2021-05-04 21:40:11 -07:00
|
|
|
debug(`generateFallbackCertificateSync: domain=${domain} cn=${cn}`);
|
2018-11-05 20:36:58 -08:00
|
|
|
|
|
|
|
|
opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${cn}\n`;
|
2021-10-06 13:16:36 -07:00
|
|
|
const configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
|
2018-11-05 19:09:58 -08:00
|
|
|
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
|
2020-10-08 14:38:52 -07:00
|
|
|
// the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176)
|
2021-10-06 13:16:36 -07:00
|
|
|
const certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`);
|
|
|
|
|
if (!safe.child_process.execSync(certCommand)) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error.message);
|
2018-11-05 19:09:58 -08:00
|
|
|
safe.fs.unlinkSync(configFile);
|
|
|
|
|
|
|
|
|
|
const cert = safe.fs.readFileSync(certFilePath, 'utf8');
|
2021-10-06 13:16:36 -07:00
|
|
|
if (!cert) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
|
2018-11-05 19:09:58 -08:00
|
|
|
safe.fs.unlinkSync(certFilePath);
|
|
|
|
|
|
|
|
|
|
const key = safe.fs.readFileSync(keyFilePath, 'utf8');
|
2021-10-06 13:16:36 -07:00
|
|
|
if (!key) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
|
2018-11-05 19:09:58 -08:00
|
|
|
safe.fs.unlinkSync(keyFilePath);
|
|
|
|
|
|
2021-10-06 13:16:36 -07:00
|
|
|
return { cert, key };
|
2018-11-05 19:09:58 -08:00
|
|
|
}
|
|
|
|
|
|
2021-08-13 17:22:28 -07:00
|
|
|
async function setFallbackCertificate(domain, fallback) {
|
2018-01-24 14:28:35 -08:00
|
|
|
assert.strictEqual(typeof domain, 'string');
|
2018-11-05 19:09:58 -08:00
|
|
|
assert(fallback && typeof fallback === 'object');
|
2018-01-26 20:30:37 -08:00
|
|
|
assert.strictEqual(typeof fallback, 'object');
|
2015-12-11 13:52:21 -08:00
|
|
|
|
2020-08-07 11:41:15 -07:00
|
|
|
debug(`setFallbackCertificate: setting certs for domain ${domain}`);
|
2021-08-13 17:22:28 -07:00
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
|
|
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
|
2015-12-11 13:52:21 -08:00
|
|
|
|
2019-03-04 19:35:22 -08:00
|
|
|
// TODO: maybe the cert is being used by the mail container
|
2021-08-17 14:04:29 -07:00
|
|
|
await reload();
|
2015-12-11 13:52:21 -08:00
|
|
|
}
|
|
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
async function restoreFallbackCertificates() {
|
|
|
|
|
const result = await domains.list();
|
2021-05-04 21:40:11 -07:00
|
|
|
|
2021-10-06 13:01:12 -07:00
|
|
|
for (const domain of result) {
|
2021-08-17 14:04:29 -07:00
|
|
|
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);
|
2021-10-06 13:01:12 -07:00
|
|
|
}
|
2021-05-04 21:40:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getFallbackCertificatePathSync(domain) {
|
|
|
|
|
assert.strictEqual(typeof domain, 'string');
|
|
|
|
|
|
2021-05-07 20:19:18 -07:00
|
|
|
const certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
|
|
|
|
|
const keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
|
2018-01-31 18:20:29 -08:00
|
|
|
|
2021-05-04 21:40:11 -07:00
|
|
|
return { certFilePath, keyFilePath };
|
2016-05-04 17:37:21 -07:00
|
|
|
}
|
|
|
|
|
|
2021-05-07 21:56:26 -07:00
|
|
|
function getAppCertificatePathSync(vhost) {
|
|
|
|
|
assert.strictEqual(typeof vhost, 'string');
|
2018-11-05 19:09:58 -08:00
|
|
|
|
2021-05-07 21:56:26 -07:00
|
|
|
const certFilePath = path.join(paths.NGINX_CERT_DIR, `${vhost}.user.cert`);
|
|
|
|
|
const keyFilePath = path.join(paths.NGINX_CERT_DIR, `${vhost}.user.key`);
|
2018-11-05 19:09:58 -08:00
|
|
|
|
2021-05-07 21:56:26 -07:00
|
|
|
return { certFilePath, keyFilePath };
|
2018-11-05 19:09:58 -08:00
|
|
|
}
|
|
|
|
|
|
2021-05-07 21:56:26 -07:00
|
|
|
function getAcmeCertificatePathSync(vhost, domainObject) {
|
2021-01-19 13:47:11 -08:00
|
|
|
assert.strictEqual(typeof vhost, 'string'); // this can contain wildcard domain (for alias domains)
|
2018-11-14 19:36:12 -08:00
|
|
|
assert.strictEqual(typeof domainObject, 'object');
|
2016-05-04 17:37:21 -07:00
|
|
|
|
2021-05-07 22:43:30 -07:00
|
|
|
let certName, certFilePath, keyFilePath, csrFilePath, acmeChallengesDir = paths.ACME_CHALLENGES_DIR;
|
2017-01-17 09:58:55 -08:00
|
|
|
|
2021-01-19 13:47:11 -08:00
|
|
|
if (vhost !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
|
2021-08-13 17:22:28 -07:00
|
|
|
certName = dns.makeWildcard(vhost).replace('*.', '_.');
|
2021-05-07 20:19:18 -07:00
|
|
|
certFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.cert`);
|
|
|
|
|
keyFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.key`);
|
2021-05-07 21:56:26 -07:00
|
|
|
csrFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.csr`);
|
2018-11-14 19:36:12 -08:00
|
|
|
} else {
|
2021-05-07 21:56:26 -07:00
|
|
|
certName = vhost;
|
2021-05-07 20:19:18 -07:00
|
|
|
certFilePath = path.join(paths.NGINX_CERT_DIR, `${vhost}.cert`);
|
|
|
|
|
keyFilePath = path.join(paths.NGINX_CERT_DIR, `${vhost}.key`);
|
2021-05-07 21:56:26 -07:00
|
|
|
csrFilePath = path.join(paths.NGINX_CERT_DIR, `${vhost}.csr`);
|
|
|
|
|
}
|
2018-09-11 22:46:17 -07:00
|
|
|
|
2021-05-07 22:43:30 -07:00
|
|
|
return { certName, certFilePath, keyFilePath, csrFilePath, acmeChallengesDir };
|
2021-05-07 21:56:26 -07:00
|
|
|
}
|
|
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
async function setAppCertificate(location, domainObject, certificate) {
|
2021-05-07 21:56:26 -07:00
|
|
|
assert.strictEqual(typeof location, 'string');
|
|
|
|
|
assert.strictEqual(typeof domainObject, 'object');
|
|
|
|
|
assert.strictEqual(typeof certificate, 'object');
|
|
|
|
|
|
2021-08-13 17:22:28 -07:00
|
|
|
const fqdn = dns.fqdn(location, domainObject);
|
2021-05-07 21:56:26 -07:00
|
|
|
const { certFilePath, keyFilePath } = getAppCertificatePathSync(fqdn);
|
|
|
|
|
|
|
|
|
|
if (certificate.cert && certificate.key) {
|
2021-08-17 14:04:29 -07:00
|
|
|
if (!safe.fs.writeFileSync(certFilePath, certificate.cert)) throw safe.error;
|
|
|
|
|
if (!safe.fs.writeFileSync(keyFilePath, certificate.key)) throw safe.error;
|
2021-05-07 21:56:26 -07:00
|
|
|
} 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}`);
|
2018-11-14 19:36:12 -08:00
|
|
|
}
|
2018-09-11 23:47:23 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
await reload();
|
2018-09-11 23:47:23 -07:00
|
|
|
}
|
|
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
async function getCertificatePath(fqdn, domain) {
|
2018-12-19 14:20:48 -08:00
|
|
|
assert.strictEqual(typeof fqdn, 'string');
|
|
|
|
|
assert.strictEqual(typeof domain, 'string');
|
2018-09-11 23:47:23 -07:00
|
|
|
|
2020-08-07 22:59:57 -07:00
|
|
|
// 1. user cert always wins
|
|
|
|
|
// 2. if using fallback provider, return that cert
|
|
|
|
|
// 3. look for LE certs
|
|
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
const domainObject = await domains.get(domain);
|
2018-11-14 19:36:12 -08:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
const appCertPath = getAppCertificatePathSync(fqdn); // user cert always wins
|
|
|
|
|
if (fs.existsSync(appCertPath.certFilePath) && fs.existsSync(appCertPath.keyFilePath)) return appCertPath;
|
2020-08-07 22:59:57 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
if (domainObject.tlsConfig.provider === 'fallback') return getFallbackCertificatePathSync(domain);
|
2020-08-07 22:59:57 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
const acmeCertPath = getAcmeCertificatePathSync(fqdn, domainObject);
|
|
|
|
|
if (fs.existsSync(acmeCertPath.certFilePath) && fs.existsSync(acmeCertPath.keyFilePath)) return acmeCertPath;
|
2018-09-11 22:46:17 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
return getFallbackCertificatePathSync(domain);
|
2017-01-17 10:21:42 -08:00
|
|
|
}
|
|
|
|
|
|
2021-05-07 21:56:26 -07:00
|
|
|
async function checkAppCertificate(vhost, domainObject) {
|
|
|
|
|
assert.strictEqual(typeof vhost, 'string'); // this can contain wildcard domain (for alias domains)
|
|
|
|
|
assert.strictEqual(typeof domainObject, 'object');
|
|
|
|
|
|
|
|
|
|
const subdomain = vhost.substr(0, vhost.length - domainObject.domain.length - 1);
|
2021-05-07 22:43:30 -07:00
|
|
|
const certificate = await apps.getCertificate(subdomain, domainObject.domain);
|
2021-05-07 21:56:26 -07:00
|
|
|
if (!certificate) return null;
|
|
|
|
|
|
|
|
|
|
const { certFilePath, keyFilePath } = getAppCertificatePathSync(vhost);
|
|
|
|
|
|
|
|
|
|
if (!safe.fs.writeFileSync(certFilePath, certificate.cert)) throw new BoxError(BoxError.FS_ERROR, `Failed to write certificate: ${safe.error.message}`);
|
|
|
|
|
if (!safe.fs.writeFileSync(keyFilePath, certificate.key)) throw new BoxError(BoxError.FS_ERROR, `Failed to write key: ${safe.error.message}`);
|
|
|
|
|
|
|
|
|
|
return { certFilePath, keyFilePath };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function checkAcmeCertificate(vhost, domainObject) {
|
|
|
|
|
assert.strictEqual(typeof vhost, 'string'); // this can contain wildcard domain (for alias domains)
|
|
|
|
|
assert.strictEqual(typeof domainObject, 'object');
|
|
|
|
|
|
|
|
|
|
const { certName, certFilePath, keyFilePath, csrFilePath } = getAcmeCertificatePathSync(vhost, domainObject);
|
|
|
|
|
|
|
|
|
|
const privateKey = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.key`);
|
|
|
|
|
const cert = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.cert`);
|
|
|
|
|
const csr = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.csr`);
|
|
|
|
|
|
|
|
|
|
if (!privateKey || !cert) return null;
|
|
|
|
|
|
|
|
|
|
if (!safe.fs.writeFileSync(keyFilePath, privateKey)) throw new BoxError(BoxError.FS_ERROR, `Failed to write private key: ${safe.error.message}`);
|
|
|
|
|
if (!safe.fs.writeFileSync(certFilePath, cert)) throw new BoxError(BoxError.FS_ERROR, `Failed to write certificate: ${safe.error.message}`);
|
|
|
|
|
|
|
|
|
|
if (csr) safe.fs.writeFileSync(csrFilePath, csr);
|
|
|
|
|
|
|
|
|
|
return { certFilePath, keyFilePath };
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-07 22:43:30 -07:00
|
|
|
async function updateCertBlobs(vhost, domainObject) {
|
|
|
|
|
assert.strictEqual(typeof vhost, 'string'); // this can contain wildcard domain (for alias domains)
|
|
|
|
|
assert.strictEqual(typeof domainObject, 'object');
|
|
|
|
|
|
|
|
|
|
const { certName, certFilePath, keyFilePath, csrFilePath } = getAcmeCertificatePathSync(vhost, domainObject);
|
|
|
|
|
|
|
|
|
|
const privateKey = safe.fs.readFileSync(keyFilePath);
|
|
|
|
|
if (!privateKey) throw new BoxError(BoxError.FS_ERROR, `Failed to read private key: ${safe.error.message}`);
|
|
|
|
|
|
|
|
|
|
const cert = safe.fs.readFileSync(certFilePath);
|
|
|
|
|
if (!cert) throw new BoxError(BoxError.FS_ERROR, `Failed to read cert: ${safe.error.message}`);
|
|
|
|
|
|
|
|
|
|
const csr = safe.fs.readFileSync(csrFilePath);
|
|
|
|
|
if (!csr) throw new BoxError(BoxError.FS_ERROR, `Failed to read csr: ${safe.error.message}`);
|
|
|
|
|
|
|
|
|
|
await blobs.set(`${blobs.CERT_PREFIX}-${certName}.key`, privateKey);
|
|
|
|
|
await blobs.set(`${blobs.CERT_PREFIX}-${certName}.cert`, cert);
|
|
|
|
|
await blobs.set(`${blobs.CERT_PREFIX}-${certName}.csr`, csr);
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
async function ensureCertificate(vhost, domain, auditSource) {
|
2018-09-12 12:50:04 -07:00
|
|
|
assert.strictEqual(typeof vhost, 'string');
|
|
|
|
|
assert.strictEqual(typeof domain, 'string');
|
2018-01-30 15:16:34 -08:00
|
|
|
assert.strictEqual(typeof auditSource, 'object');
|
2015-12-11 14:15:23 -08:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
const domainObject = await domains.get(domain);
|
2016-09-12 01:21:51 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
let bundle = await checkAppCertificate(vhost, domainObject);
|
|
|
|
|
if (bundle) return { bundle, renewed: false };
|
2020-08-07 22:59:57 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
if (domainObject.tlsConfig.provider === 'fallback') {
|
|
|
|
|
debug(`ensureCertificate: ${vhost} will use fallback certs`);
|
2020-08-10 14:54:37 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
return { bundle: getFallbackCertificatePathSync(domain), renewed: false };
|
|
|
|
|
}
|
2020-08-07 22:59:57 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
const { acmeApi, apiOptions } = await getAcmeApi(domainObject);
|
|
|
|
|
let notAfter = null;
|
2021-07-15 09:50:11 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
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`);
|
|
|
|
|
}
|
2015-12-11 14:15:23 -08:00
|
|
|
|
2021-11-16 22:56:35 -08:00
|
|
|
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
|
2018-02-02 21:21:51 -08:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
const acmePaths = getAcmeCertificatePathSync(vhost, domainObject);
|
2021-09-07 09:34:23 -07:00
|
|
|
let [error] = await safe(acmeApi.getCertificate(vhost, domain, acmePaths, apiOptions));
|
2021-08-17 14:04:29 -07:00
|
|
|
debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${acmePaths.certFilePath || 'null'}`);
|
2019-10-03 10:36:57 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
await safe(eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '', notAfter }));
|
2015-12-14 17:09:40 -08:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
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 };
|
|
|
|
|
}
|
2019-10-03 10:46:03 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
if (!error) {
|
|
|
|
|
[error] = await safe(updateCertBlobs(vhost, domainObject));
|
|
|
|
|
if (!error) return { bundle: { certFilePath: acmePaths.certFilePath, keyFilePath: acmePaths.keyFilePath }, renewed: true };
|
|
|
|
|
}
|
2019-10-01 14:04:39 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`);
|
2019-10-03 10:36:57 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
return { bundle: getFallbackCertificatePathSync(domain), renewed: false };
|
2015-12-11 14:15:23 -08:00
|
|
|
}
|
2018-01-30 12:23:27 -08:00
|
|
|
|
2022-01-16 09:43:52 -08:00
|
|
|
async function writeDashboardNginxConfig(vhost, bundle) {
|
2018-01-30 12:23:27 -08:00
|
|
|
assert.strictEqual(typeof vhost, 'string');
|
2022-01-16 09:43:52 -08:00
|
|
|
assert.strictEqual(typeof bundle, 'object');
|
2018-01-30 12:23:27 -08:00
|
|
|
|
2021-04-16 11:17:13 -07:00
|
|
|
const data = {
|
2018-01-30 12:23:27 -08:00
|
|
|
sourceDir: path.resolve(__dirname, '..'),
|
2020-09-23 22:13:02 -07:00
|
|
|
vhost: vhost,
|
2019-07-25 11:26:53 -07:00
|
|
|
hasIPv6: sysinfo.hasIPv6(),
|
2021-05-05 13:13:01 -07:00
|
|
|
endpoint: 'dashboard',
|
2018-01-30 16:16:10 -08:00
|
|
|
certFilePath: bundle.certFilePath,
|
|
|
|
|
keyFilePath: bundle.keyFilePath,
|
2020-11-09 20:34:48 -08:00
|
|
|
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n'),
|
2021-04-16 11:17:13 -07:00
|
|
|
proxyAuth: { enabled: false, id: null, location: nginxLocation('/') },
|
2021-09-22 09:13:16 -07:00
|
|
|
ocsp: await isOcspEnabled(bundle.certFilePath)
|
2018-01-30 12:23:27 -08:00
|
|
|
};
|
2021-04-16 11:17:13 -07:00
|
|
|
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
2022-01-16 09:43:52 -08:00
|
|
|
const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${vhost}.conf`);
|
2018-01-30 12:23:27 -08:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) throw new BoxError(BoxError.FS_ERROR, safe.error);
|
2018-01-30 12:23:27 -08:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
await reload();
|
2018-01-30 12:23:27 -08:00
|
|
|
}
|
|
|
|
|
|
2022-01-16 10:16:14 -08:00
|
|
|
async function writeDashboardConfig(domainObject) {
|
|
|
|
|
assert.strictEqual(typeof domainObject, 'object');
|
2018-12-13 22:15:08 -08:00
|
|
|
|
2022-01-16 10:16:14 -08:00
|
|
|
debug(`writeDashboardConfig: writing admin config for ${domainObject.domain}`);
|
2019-09-30 11:52:23 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
const dashboardFqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject);
|
|
|
|
|
const bundle = await getCertificatePath(dashboardFqdn, domainObject.domain);
|
2018-12-13 22:15:08 -08:00
|
|
|
|
2022-01-16 09:43:52 -08:00
|
|
|
await writeDashboardNginxConfig(dashboardFqdn, bundle);
|
2018-01-30 13:54:13 -08:00
|
|
|
}
|
2018-01-30 12:23:27 -08:00
|
|
|
|
2022-01-16 12:22:29 -08:00
|
|
|
function getNginxConfigFilename(app, fqdn, type) {
|
|
|
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
|
assert.strictEqual(typeof fqdn, 'string');
|
|
|
|
|
assert.strictEqual(typeof type, 'string');
|
|
|
|
|
|
|
|
|
|
let nginxConfigFilenameSuffix = '';
|
|
|
|
|
|
2022-02-07 13:48:04 -08:00
|
|
|
if (type === apps.LOCATION_TYPE_ALIAS) {
|
2022-01-16 12:22:29 -08:00
|
|
|
nginxConfigFilenameSuffix = `-alias-${fqdn.replace('*', '_')}`;
|
2022-02-07 13:48:04 -08:00
|
|
|
} else if (type === apps.LOCATION_TYPE_SECONDARY) {
|
2022-01-16 12:22:29 -08:00
|
|
|
nginxConfigFilenameSuffix = `-secondary-${fqdn}`;
|
2022-02-07 13:48:04 -08:00
|
|
|
} else if (type === apps.LOCATION_TYPE_REDIRECT) {
|
2022-01-16 12:22:29 -08:00
|
|
|
nginxConfigFilenameSuffix = `-redirect-${fqdn}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}${nginxConfigFilenameSuffix}.conf`);
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-16 10:40:16 -08:00
|
|
|
async function writeAppNginxConfig(app, fqdn, type, bundle) {
|
2018-06-29 16:14:13 +02:00
|
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
|
assert.strictEqual(typeof fqdn, 'string');
|
2022-01-16 10:40:16 -08:00
|
|
|
assert.strictEqual(typeof type, 'string');
|
2018-06-29 16:14:13 +02:00
|
|
|
assert.strictEqual(typeof bundle, 'object');
|
|
|
|
|
|
2021-04-16 11:17:13 -07:00
|
|
|
const data = {
|
2018-06-29 16:14:13 +02:00
|
|
|
sourceDir: path.resolve(__dirname, '..'),
|
|
|
|
|
vhost: fqdn,
|
2019-07-25 11:26:53 -07:00
|
|
|
hasIPv6: sysinfo.hasIPv6(),
|
2022-01-16 10:40:16 -08:00
|
|
|
ip: null,
|
|
|
|
|
port: null,
|
|
|
|
|
endpoint: null,
|
|
|
|
|
redirectTo: null,
|
2018-06-29 16:14:13 +02:00
|
|
|
certFilePath: bundle.certFilePath,
|
|
|
|
|
keyFilePath: bundle.keyFilePath,
|
2019-10-13 18:22:03 -07:00
|
|
|
robotsTxtQuoted: null,
|
2019-10-14 16:59:22 -07:00
|
|
|
cspQuoted: null,
|
2020-11-09 20:34:48 -08:00
|
|
|
hideHeaders: [],
|
2022-01-20 16:57:30 -08:00
|
|
|
proxyAuth: { enabled: false },
|
2022-06-06 20:04:22 +02:00
|
|
|
upstreamUri: '', // only for endpoint === external
|
2021-09-22 09:13:16 -07:00
|
|
|
ocsp: await isOcspEnabled(bundle.certFilePath)
|
2018-06-29 16:14:13 +02:00
|
|
|
};
|
2022-01-16 10:40:16 -08:00
|
|
|
|
2022-02-07 13:48:04 -08:00
|
|
|
if (type === apps.LOCATION_TYPE_PRIMARY || type === apps.LOCATION_TYPE_ALIAS || type === apps.LOCATION_TYPE_SECONDARY) {
|
2022-01-14 22:40:51 -08:00
|
|
|
data.endpoint = 'app';
|
2022-06-06 20:04:22 +02:00
|
|
|
|
|
|
|
|
if (app.manifest.id === constants.RELAY_APPSTORE_ID) {
|
|
|
|
|
data.endpoint = 'external';
|
|
|
|
|
data.upstreamUri = 'http://example.com';
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-14 22:40:51 -08:00
|
|
|
// maybe these should become per domain at some point
|
2022-01-16 10:40:16 -08:00
|
|
|
const reverseProxyConfig = app.reverseProxyConfig || {}; // some of our code uses fake app objects
|
|
|
|
|
if (reverseProxyConfig.robotsTxt) data.robotsTxtQuoted = JSON.stringify(app.reverseProxyConfig.robotsTxt);
|
|
|
|
|
if (reverseProxyConfig.csp) {
|
|
|
|
|
data.cspQuoted = `"${app.reverseProxyConfig.csp}"`;
|
|
|
|
|
data.hideHeaders = [ 'Content-Security-Policy' ];
|
|
|
|
|
if (reverseProxyConfig.csp.includes('frame-ancestors ')) data.hideHeaders.push('X-Frame-Options');
|
|
|
|
|
}
|
2022-02-07 13:48:04 -08:00
|
|
|
if (type === apps.LOCATION_TYPE_PRIMARY || type == apps.LOCATION_TYPE_ALIAS) {
|
2022-01-14 22:40:51 -08:00
|
|
|
data.proxyAuth = {
|
|
|
|
|
enabled: app.sso && app.manifest.addons && app.manifest.addons.proxyAuth,
|
|
|
|
|
id: app.id,
|
|
|
|
|
location: nginxLocation(safe.query(app.manifest, 'addons.proxyAuth.path') || '/')
|
|
|
|
|
};
|
|
|
|
|
data.ip = app.containerIp;
|
|
|
|
|
data.port = app.manifest.httpPort;
|
2022-02-07 13:48:04 -08:00
|
|
|
} else if (type === apps.LOCATION_TYPE_SECONDARY) {
|
2022-01-14 22:40:51 -08:00
|
|
|
data.ip = app.containerIp;
|
|
|
|
|
const secondaryDomain = app.secondaryDomains.find(sd => sd.fqdn === fqdn);
|
|
|
|
|
data.port = app.manifest.httpPorts[secondaryDomain.environmentVariable].containerPort;
|
|
|
|
|
}
|
2022-02-07 13:48:04 -08:00
|
|
|
} else if (type === apps.LOCATION_TYPE_REDIRECT) {
|
2022-01-16 10:40:16 -08:00
|
|
|
data.proxyAuth = { enabled: false, id: app.id, location: nginxLocation('/') };
|
|
|
|
|
data.endpoint = 'redirect';
|
|
|
|
|
data.redirectTo = app.fqdn;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-16 11:17:13 -07:00
|
|
|
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
2022-01-16 12:22:29 -08:00
|
|
|
const filename = getNginxConfigFilename(app, fqdn, type);
|
|
|
|
|
debug(`writeAppNginxConfig: writing config for "${fqdn}" to ${filename} with options ${JSON.stringify(data)}`);
|
|
|
|
|
if (!safe.fs.writeFileSync(filename, nginxConf)) {
|
2022-01-16 10:40:16 -08:00
|
|
|
debug(`Error creating nginx config for "${app.fqdn}" : ${safe.error.message}`);
|
2021-08-17 14:04:29 -07:00
|
|
|
throw new BoxError(BoxError.FS_ERROR, safe.error);
|
2018-06-29 16:14:13 +02:00
|
|
|
}
|
|
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
await reload();
|
2018-06-29 16:14:13 +02:00
|
|
|
}
|
|
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
async function writeAppConfig(app) {
|
2019-09-09 21:41:55 -07:00
|
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
|
|
2022-02-07 13:48:04 -08:00
|
|
|
const appDomains = [{ domain: app.domain, fqdn: app.fqdn, type: apps.LOCATION_TYPE_PRIMARY }]
|
|
|
|
|
.concat(app.secondaryDomains.map(sd => { return { domain: sd.domain, fqdn: sd.fqdn, type: apps.LOCATION_TYPE_SECONDARY }; }))
|
|
|
|
|
.concat(app.redirectDomains.map(rd => { return { domain: rd.domain, fqdn: rd.fqdn, type: apps.LOCATION_TYPE_REDIRECT }; }))
|
|
|
|
|
.concat(app.aliasDomains.map(ad => { return { domain: ad.domain, fqdn: ad.fqdn, type: apps.LOCATION_TYPE_ALIAS }; }));
|
2019-09-09 21:41:55 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
for (const appDomain of appDomains) {
|
|
|
|
|
const bundle = await getCertificatePath(appDomain.fqdn, appDomain.domain);
|
2022-01-16 10:40:16 -08:00
|
|
|
await writeAppNginxConfig(app, appDomain.fqdn, appDomain.type, bundle);
|
2021-08-17 14:04:29 -07:00
|
|
|
}
|
2019-09-09 21:41:55 -07:00
|
|
|
}
|
|
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
async function configureApp(app, auditSource) {
|
2018-01-30 13:54:13 -08:00
|
|
|
assert.strictEqual(typeof app, 'object');
|
2018-01-30 15:16:34 -08:00
|
|
|
assert.strictEqual(typeof auditSource, 'object');
|
2018-01-30 12:23:27 -08:00
|
|
|
|
2022-01-16 10:28:49 -08:00
|
|
|
const appDomains = [{ domain: app.domain, fqdn: app.fqdn }]
|
2022-02-16 20:32:04 -08:00
|
|
|
.concat(app.secondaryDomains.map(sd => { return { domain: sd.domain, fqdn: sd.fqdn }; }))
|
2022-01-16 10:28:49 -08:00
|
|
|
.concat(app.redirectDomains.map(rd => { return { domain: rd.domain, fqdn: rd.fqdn }; }))
|
|
|
|
|
.concat(app.aliasDomains.map(ad => { return { domain: ad.domain, fqdn: ad.fqdn }; }));
|
2018-06-29 16:14:13 +02:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
for (const appDomain of appDomains) {
|
2022-01-16 10:28:49 -08:00
|
|
|
await ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource);
|
2021-08-17 14:04:29 -07:00
|
|
|
}
|
2022-01-16 10:28:49 -08:00
|
|
|
|
2022-01-16 10:40:16 -08:00
|
|
|
await writeAppConfig(app);
|
2018-01-30 12:23:27 -08:00
|
|
|
}
|
|
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
async function unconfigureApp(app) {
|
2018-01-30 12:23:27 -08:00
|
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
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}`);
|
2018-06-29 19:04:48 +02:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
for (const filename of configFilenames) {
|
|
|
|
|
if (!filename.startsWith(app.id)) continue;
|
|
|
|
|
|
|
|
|
|
safe.fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, filename));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await reload();
|
2018-01-30 12:23:27 -08:00
|
|
|
}
|
|
|
|
|
|
2021-08-25 15:52:05 -07:00
|
|
|
async function renewCerts(options, auditSource, progressCallback) {
|
2018-10-24 13:01:45 -07:00
|
|
|
assert.strictEqual(typeof options, 'object');
|
2018-01-30 16:16:10 -08:00
|
|
|
assert.strictEqual(typeof auditSource, 'object');
|
2018-12-10 20:20:53 -08:00
|
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
2018-01-30 16:16:10 -08:00
|
|
|
|
2021-08-25 15:52:05 -07:00
|
|
|
const allApps = await apps.list();
|
2018-01-30 16:16:10 -08:00
|
|
|
|
2021-08-25 15:52:05 -07:00
|
|
|
let appDomains = [];
|
2018-08-25 11:04:49 +02:00
|
|
|
|
2021-08-25 15:52:05 -07:00
|
|
|
// add webadmin and mail domain
|
|
|
|
|
if (settings.mailFqdn() === settings.dashboardFqdn()) {
|
|
|
|
|
appDomains.push({ domain: settings.dashboardDomain(), fqdn: settings.dashboardFqdn(), type: 'webadmin+mail', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.dashboardFqdn()}.conf`) });
|
|
|
|
|
} else {
|
|
|
|
|
appDomains.push({ domain: settings.dashboardDomain(), fqdn: settings.dashboardFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.dashboardFqdn()}.conf`) });
|
|
|
|
|
appDomains.push({ domain: settings.mailDomain(), fqdn: settings.mailFqdn(), type: 'mail' });
|
|
|
|
|
}
|
2020-01-26 16:05:23 -08:00
|
|
|
|
2021-08-25 15:52:05 -07:00
|
|
|
for (const app of allApps) {
|
|
|
|
|
if (app.runState === apps.RSTATE_STOPPED) continue; // do not renew certs of stopped apps
|
2018-08-25 11:04:49 +02:00
|
|
|
|
2022-04-04 10:30:27 -07:00
|
|
|
appDomains = appDomains.concat([{ app, domain: app.domain, fqdn: app.fqdn, type: apps.LOCATION_TYPE_PRIMARY, nginxConfigFilename: getNginxConfigFilename(app, app.fqdn, apps.LOCATION_TYPE_PRIMARY) }])
|
2022-02-07 13:48:04 -08:00
|
|
|
.concat(app.secondaryDomains.map(sd => { return { app, domain: sd.domain, fqdn: sd.fqdn, type: apps.LOCATION_TYPE_SECONDARY, nginxConfigFilename: getNginxConfigFilename(app, sd.fqdn, apps.LOCATION_TYPE_SECONDARY) }; }))
|
|
|
|
|
.concat(app.redirectDomains.map(rd => { return { app, domain: rd.domain, fqdn: rd.fqdn, type: apps.LOCATION_TYPE_REDIRECT, nginxConfigFilename: getNginxConfigFilename(app, rd.fqdn, apps.LOCATION_TYPE_REDIRECT) }; }))
|
|
|
|
|
.concat(app.aliasDomains.map(ad => { return { app, domain: ad.domain, fqdn: ad.fqdn, type: apps.LOCATION_TYPE_ALIAS, nginxConfigFilename: getNginxConfigFilename(app, ad.fqdn, apps.LOCATION_TYPE_ALIAS) }; }));
|
2021-08-25 15:52:05 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.domain) appDomains = appDomains.filter(function (appDomain) { return appDomain.domain === options.domain; });
|
|
|
|
|
|
2021-08-30 15:21:30 -07:00
|
|
|
let progress = 1, renewedCerts = [];
|
2021-08-25 15:52:05 -07:00
|
|
|
|
|
|
|
|
for (const appDomain of appDomains) {
|
|
|
|
|
progressCallback({ percent: progress, message: `Ensuring certs of ${appDomain.fqdn}` });
|
|
|
|
|
progress += Math.round(100/appDomains.length);
|
|
|
|
|
|
|
|
|
|
const { bundle, renewed } = await ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource);
|
|
|
|
|
|
2021-08-30 15:21:30 -07:00
|
|
|
if (renewed) renewedCerts.push(appDomain.fqdn);
|
2021-08-25 15:52:05 -07:00
|
|
|
|
|
|
|
|
if (appDomain.type === 'mail') continue; // 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') || '';
|
2021-09-23 17:47:49 -07:00
|
|
|
if (currentNginxConfig.includes(bundle.certFilePath)) continue;
|
2021-08-25 15:52:05 -07:00
|
|
|
|
|
|
|
|
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') {
|
2022-01-16 09:43:52 -08:00
|
|
|
await writeDashboardNginxConfig(settings.dashboardFqdn(), bundle);
|
2021-08-25 15:52:05 -07:00
|
|
|
} else {
|
2022-01-16 10:40:16 -08:00
|
|
|
await writeAppNginxConfig(appDomain.app, appDomain.fqdn, appDomain.type, bundle);
|
2021-08-25 15:52:05 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-30 15:21:30 -07:00
|
|
|
debug(`renewCerts: Renewed certs of ${JSON.stringify(renewedCerts)}`);
|
|
|
|
|
if (renewedCerts.length === 0) return;
|
2021-08-25 15:52:05 -07:00
|
|
|
|
2021-08-30 15:21:30 -07:00
|
|
|
if (renewedCerts.includes(settings.mailFqdn())) await mail.handleCertChanged();
|
2021-08-25 15:52:05 -07:00
|
|
|
|
|
|
|
|
await reload(); // reload nginx if any certs were updated but the config was not rewritten
|
|
|
|
|
|
|
|
|
|
// restart tls apps on cert change
|
2021-08-30 15:21:30 -07:00
|
|
|
const tlsApps = allApps.filter(app => app.manifest.addons && app.manifest.addons.tls && renewedCerts.includes(app.fqdn));
|
2021-08-25 15:52:05 -07:00
|
|
|
for (const app of tlsApps) {
|
|
|
|
|
await apps.restart(app, auditSource);
|
|
|
|
|
}
|
2018-01-30 16:16:10 -08:00
|
|
|
}
|
|
|
|
|
|
2022-02-24 19:52:51 -08:00
|
|
|
async function cleanupCerts(auditSource) {
|
|
|
|
|
assert.strictEqual(typeof auditSource, 'object');
|
|
|
|
|
|
2021-05-18 13:28:48 -07:00
|
|
|
const filenames = await fs.promises.readdir(paths.NGINX_CERT_DIR);
|
|
|
|
|
const certFilenames = filenames.filter(f => f.endsWith('.cert'));
|
2021-06-01 09:09:16 -07:00
|
|
|
const now = new Date();
|
2021-05-18 13:28:48 -07:00
|
|
|
|
2021-09-23 17:39:59 -07:00
|
|
|
debug('cleanupCerts: start');
|
|
|
|
|
|
2022-02-24 19:52:51 -08:00
|
|
|
const fqdns = [];
|
|
|
|
|
|
2021-05-18 13:28:48 -07:00
|
|
|
for (const certFilename of certFilenames) {
|
|
|
|
|
const certFilePath = path.join(paths.NGINX_CERT_DIR, certFilename);
|
2021-06-01 09:09:16 -07:00
|
|
|
const notAfter = getExpiryDate(certFilePath);
|
|
|
|
|
if (!notAfter) continue; // some error
|
|
|
|
|
|
|
|
|
|
if (now - notAfter >= (60 * 60 * 24 * 30 * 6 * 1000)) { // expired 6 months ago
|
2021-05-18 13:28:48 -07:00
|
|
|
const fqdn = certFilename.replace(/\.cert$/, '');
|
|
|
|
|
debug(`cleanupCerts: deleting certs of ${fqdn}`);
|
|
|
|
|
|
2022-01-28 09:52:03 -08:00
|
|
|
// it is safe to delete the certs of stopped apps because their nginx configs are removed
|
2021-05-18 13:28:48 -07:00
|
|
|
safe.fs.unlinkSync(certFilePath);
|
|
|
|
|
safe.fs.unlinkSync(path.join(paths.NGINX_CERT_DIR, `${fqdn}.key`));
|
|
|
|
|
safe.fs.unlinkSync(path.join(paths.NGINX_CERT_DIR, `${fqdn}.csr`));
|
|
|
|
|
|
|
|
|
|
await blobs.del(`${blobs.CERT_PREFIX}-${fqdn}.key`);
|
|
|
|
|
await blobs.del(`${blobs.CERT_PREFIX}-${fqdn}.cert`);
|
|
|
|
|
await blobs.del(`${blobs.CERT_PREFIX}-${fqdn}.csr`);
|
2022-02-24 19:52:51 -08:00
|
|
|
|
|
|
|
|
fqdns.push(fqdn);
|
2021-05-18 13:28:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
2021-09-23 17:39:59 -07:00
|
|
|
|
2022-02-24 19:52:51 -08:00
|
|
|
if (fqdns.length) await safe(eventlog.add(eventlog.ACTION_CERTIFICATE_CLEANUP, auditSource, { domains: fqdns }));
|
|
|
|
|
|
2021-09-23 17:39:59 -07:00
|
|
|
debug('cleanupCerts: done');
|
2021-05-18 13:28:48 -07:00
|
|
|
}
|
|
|
|
|
|
2021-08-25 15:52:05 -07:00
|
|
|
async function checkCerts(options, auditSource, progressCallback) {
|
2021-05-18 13:28:48 -07:00
|
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
|
assert.strictEqual(typeof auditSource, 'object');
|
|
|
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
|
|
2021-08-25 15:52:05 -07:00
|
|
|
await renewCerts(options, auditSource, progressCallback);
|
2022-02-24 19:52:51 -08:00
|
|
|
await cleanupCerts(auditSource);
|
2021-05-18 13:28:48 -07:00
|
|
|
}
|
|
|
|
|
|
2018-01-30 12:23:27 -08:00
|
|
|
function removeAppConfigs() {
|
2021-06-27 08:58:33 -07:00
|
|
|
const dashboardConfigFilename = `${settings.dashboardFqdn()}.conf`;
|
|
|
|
|
|
2022-02-17 11:08:22 -08:00
|
|
|
debug('removeAppConfigs: reomving nginx configs of apps');
|
|
|
|
|
|
2021-06-27 08:58:33 -07:00
|
|
|
// remove all configs which are not the default or current dashboard
|
2018-11-10 22:02:42 -08:00
|
|
|
for (let appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
|
2021-06-27 08:58:33 -07:00
|
|
|
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && appConfigFile !== dashboardConfigFilename) {
|
2018-11-10 22:02:42 -08:00
|
|
|
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
|
|
|
|
|
}
|
2018-01-30 12:23:27 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
async function writeDefaultConfig(options) {
|
2020-09-23 22:13:02 -07:00
|
|
|
assert.strictEqual(typeof options, 'object');
|
2018-01-30 12:23:27 -08:00
|
|
|
|
2020-09-23 22:13:02 -07:00
|
|
|
const certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
|
|
|
|
|
const keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
|
2018-01-30 12:23:27 -08:00
|
|
|
|
|
|
|
|
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
2019-09-30 15:28:05 -07:00
|
|
|
debug('writeDefaultConfig: create new cert');
|
2018-01-30 12:23:27 -08:00
|
|
|
|
2020-09-23 22:13:02 -07:00
|
|
|
const cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
|
2020-10-08 14:38:52 -07:00
|
|
|
// 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`)) {
|
2019-09-30 15:28:05 -07:00
|
|
|
debug(`writeDefaultConfig: could not generate certificate: ${safe.error.message}`);
|
2021-08-17 14:04:29 -07:00
|
|
|
throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
|
2018-11-23 11:39:00 -08:00
|
|
|
}
|
2018-01-30 12:23:27 -08:00
|
|
|
}
|
|
|
|
|
|
2020-09-23 22:13:02 -07:00
|
|
|
const data = {
|
|
|
|
|
sourceDir: path.resolve(__dirname, '..'),
|
|
|
|
|
vhost: '',
|
|
|
|
|
hasIPv6: sysinfo.hasIPv6(),
|
|
|
|
|
endpoint: options.activated ? 'ip' : 'setup',
|
|
|
|
|
certFilePath,
|
|
|
|
|
keyFilePath,
|
2020-11-09 20:34:48 -08:00
|
|
|
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n'),
|
2021-04-16 11:17:13 -07:00
|
|
|
proxyAuth: { enabled: false, id: null, location: nginxLocation('/') },
|
|
|
|
|
ocsp: false // self-signed cert
|
2020-09-23 22:13:02 -07:00
|
|
|
};
|
|
|
|
|
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
|
|
|
|
const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_DEFAULT_CONFIG_FILE_NAME);
|
2020-08-13 14:00:55 -07:00
|
|
|
|
2020-10-07 14:47:51 -07:00
|
|
|
debug(`writeDefaultConfig: writing configs for endpoint "${data.endpoint}"`);
|
|
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) throw new BoxError(BoxError.FS_ERROR, safe.error);
|
2020-08-13 14:00:55 -07:00
|
|
|
|
2021-08-17 14:04:29 -07:00
|
|
|
await reload();
|
2020-08-13 14:00:55 -07:00
|
|
|
}
|