Files
cloudron-box/src/reverseproxy.js

638 lines
28 KiB
JavaScript
Raw Normal View History

2015-12-10 13:31:47 -08:00
'use strict';
exports = module.exports = {
ReverseProxyError: ReverseProxyError,
setFallbackCertificate: setFallbackCertificate,
getFallbackCertificate: getFallbackCertificate,
2017-01-17 09:57:15 -08:00
generateFallbackCertificateSync: generateFallbackCertificateSync,
setAppCertificateSync: setAppCertificateSync,
validateCertificate: validateCertificate,
2017-01-17 09:57:15 -08:00
getCertificate: getCertificate,
ensureCertificate: ensureCertificate,
2016-06-22 13:48:07 -05:00
2017-01-17 09:57:15 -08:00
renewAll: renewAll,
2018-10-24 13:01:45 -07:00
renewCerts: renewCerts,
2017-01-17 09:57:15 -08:00
// the 'configure' functions always ensure a certificate
configureDefaultServer: configureDefaultServer,
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
writeAdminConfig: writeAdminConfig,
reload: reload,
removeAppConfigs: removeAppConfigs,
2016-06-22 13:48:07 -05:00
// exported for testing
2018-09-10 20:41:38 -07:00
_getCertApi: getCertApi
};
2018-09-10 15:19:10 -07:00
var acme2 = require('./cert/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'),
2015-12-14 12:28:00 -08:00
async = require('async'),
2015-12-13 19:06:19 -08:00
caas = require('./cert/caas.js'),
2015-12-10 13:31:47 -08:00
config = require('./config.js'),
2015-12-11 13:52:21 -08:00
constants = require('./constants.js'),
crypto = require('crypto'),
2018-10-31 15:41:02 -07:00
debug = require('debug')('box:reverseproxy'),
domains = require('./domains.js'),
ejs = require('ejs'),
2016-04-30 22:27:33 -07:00
eventlog = require('./eventlog.js'),
2016-12-05 17:01:23 +01:00
fallback = require('./cert/fallback.js'),
2015-12-11 13:52:21 -08:00
fs = require('fs'),
2016-03-19 20:40:03 -07:00
mailer = require('./mailer.js'),
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'),
platform = require('./platform.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
shell = require('./shell.js'),
2018-04-29 10:58:45 -07:00
users = require('./users.js'),
util = require('util');
2015-12-10 13:31:47 -08:00
2018-12-13 21:52:42 -08:00
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
function ReverseProxyError(reason, errorOrMessage) {
2015-12-11 13:43:33 -08:00
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(ReverseProxyError, Error);
ReverseProxyError.INTERNAL_ERROR = 'Internal Error';
ReverseProxyError.INVALID_CERT = 'Invalid certificate';
ReverseProxyError.NOT_FOUND = 'Not Found';
2015-12-11 13:43:33 -08:00
2018-11-14 19:36:12 -08:00
function getCertApi(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
2018-11-14 19:36:12 -08:00
if (domainObject.tlsConfig.provider === 'fallback') return callback(null, fallback, { fallback: true });
2016-12-05 17:01:23 +01:00
2018-11-14 19:36:12 -08:00
var api = domainObject.tlsConfig.provider === 'caas' ? caas : acme2;
2015-12-14 12:28:00 -08:00
2018-11-14 19:36:12 -08:00
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
if (domainObject.tlsConfig.provider !== 'caas') { // matches 'le-prod' or 'letsencrypt-prod'
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null;
options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
options.wildcard = !!domainObject.tlsConfig.wildcard;
}
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
users.getOwner(function (error, owner) {
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
2015-12-17 13:17:46 -08:00
2018-11-14 19:36:12 -08:00
callback(null, api, options);
2015-12-14 12:28:00 -08:00
});
}
function isExpiringSync(certFilePath, hours) {
assert.strictEqual(typeof certFilePath, 'string');
2016-03-18 22:59:51 -07:00
assert.strictEqual(typeof hours, 'number');
2016-03-19 12:50:31 -07:00
if (!fs.existsSync(certFilePath)) return 2; // not found
2016-03-19 13:22:38 -07:00
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
2016-03-18 22:59:51 -07:00
2018-11-23 11:26:18 -08:00
if (!result) return 3; // some error
2016-03-21 08:25:10 -07:00
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
2016-03-14 22:50:06 -07:00
return result.status === 1; // 1 - expired 0 - not expired
}
// 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');
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof apiOptions, 'object');
if (!fs.existsSync(certFilePath)) return false; // not found
if (apiOptions.fallback) return certFilePath.includes('.host.cert');
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
if (!subjectAndIssuer) return false; // something bad happenned
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
const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1];
2018-11-15 10:45:27 -08:00
const isWildcardCert = domain.includes('*');
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt Authority');
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);
const mismatch = issuerMismatch || wildcardMismatch;
2018-11-15 10:45:27 -08:00
debug(`providerMatchesSync: ${certFilePath} subject=${subject} domain=${domain} issuer=${issuer} wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} match=${!mismatch}`);
return !mismatch;
}
// 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) {
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;
2018-01-26 19:31:06 -08:00
// check for empty cert and key strings
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
const fqdn = domains.fqdn(location, domainObject);
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
if (result === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject:' + safe.error.message);
2018-11-05 21:26:53 -08:00
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${fqdn}`);
// 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 });
if (certModulus === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Unable to get cert modulus: ${safe.error.message}`);
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (keyModulus === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Unable to get key modulus: ${safe.error.message}`);
if (certModulus !== keyModulus) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Key does not match the certificate.');
2017-11-27 10:39:42 -08:00
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Certificate has expired.');
return null;
}
2015-12-11 13:52:21 -08:00
function reload(callback) {
if (process.env.BOX_ENV === 'test') return callback();
2018-11-25 14:57:17 -08:00
shell.sudo('reload', [ RELOAD_NGINX_CMD ], {}, callback);
}
function generateFallbackCertificateSync(domainObject) {
assert.strictEqual(typeof domainObject, 'object');
const domain = domainObject.domain;
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`);
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
// 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;
2018-11-05 20:36:58 -08:00
let cn = domainObject.config.hyphenatedSubdomains ? domains.parentDomain(domain) : domain;
debug(`generateFallbackCertificateSync: domain=${domainObject.domain} cn=${cn} hyphenated=${domainObject.config.hyphenatedSubdomains}`);
opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${cn}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
2018-11-05 20:36:58 -08:00
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`);
if (!safe.child_process.execSync(certCommand)) return { error: new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message) };
safe.fs.unlinkSync(configFile);
const cert = safe.fs.readFileSync(certFilePath, 'utf8');
if (!cert) return { error: safe.error };
safe.fs.unlinkSync(certFilePath);
const key = safe.fs.readFileSync(keyFilePath, 'utf8');
if (!key) return { error: safe.error };
safe.fs.unlinkSync(keyFilePath);
return { cert: cert, key: key, error: null };
}
function setFallbackCertificate(domain, fallback, callback) {
2018-01-24 14:28:35 -08:00
assert.strictEqual(typeof domain, 'string');
assert(fallback && typeof fallback === 'object');
assert.strictEqual(typeof fallback, 'object');
2015-12-11 13:52:21 -08:00
assert.strictEqual(typeof callback, 'function');
if (fallback.restricted) { // restricted certs are not backed up
2018-11-05 20:36:58 -08:00
debug(`setFallbackCertificate: setting restricted certs for domain ${domain}`);
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
} else {
2018-11-05 20:36:58 -08:00
debug(`setFallbackCertificate: setting certs for domain ${domain}`);
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
}
2015-12-11 13:52:21 -08:00
2018-12-11 11:02:32 -08:00
platform.handleCertChanged('*.' + domain, function (error) {
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
2015-12-11 13:52:21 -08:00
2018-12-11 11:02:32 -08:00
reload(function (error) {
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
return callback(null);
});
2015-12-11 13:52:21 -08:00
});
}
2018-01-24 14:28:35 -08:00
function getFallbackCertificate(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// check for any pre-provisioned (caas) certs. they get first priority
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
var keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath, type: 'provisioned' });
// check for auto-generated or user set fallback certs
certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
callback(null, { certFilePath, keyFilePath, type: 'fallback' });
}
2018-11-05 22:36:16 -08:00
function setAppCertificateSync(location, domainObject, certificate) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domainObject, 'object');
2018-11-05 22:36:16 -08:00
assert.strictEqual(typeof certificate, 'object');
let fqdn = domains.fqdn(location, domainObject);
2018-11-05 22:36:16 -08:00
if (certificate.cert && certificate.key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), certificate.cert)) return safe.error;
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), certificate.key)) return safe.error;
} else { // remove existing cert/key
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`))) debug('Error removing key: ' + safe.error.message);
}
return null;
}
2018-11-14 19:36:12 -08:00
function getCertificateByHostname(hostname, domainObject, callback) {
assert.strictEqual(typeof hostname, 'string');
2018-11-14 19:36:12 -08:00
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
let certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.cert`);
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
2018-11-14 19:36:12 -08:00
if (hostname !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
let certName = domains.makeWildcard(hostname).replace('*.', '_.');
certFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.key`);
2018-11-14 19:36:12 -08:00
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
} else {
certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.key`);
2018-09-11 22:46:17 -07:00
2018-11-14 19:36:12 -08:00
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
}
2018-11-14 19:36:12 -08:00
callback(null);
}
2018-12-19 14:20:48 -08:00
function getCertificate(fqdn, domain, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
2018-12-19 14:20:48 -08:00
domains.get(domain, function (error, domainObject) {
2018-11-14 19:36:12 -08:00
if (error) return callback(error);
2018-12-19 14:20:48 -08:00
getCertificateByHostname(fqdn, domainObject, function (error, result) {
2018-11-14 19:36:12 -08:00
if (error || result) return callback(error, result);
2018-09-11 22:46:17 -07:00
2018-12-19 14:20:48 -08:00
return getFallbackCertificate(domain, callback);
2018-11-14 19:36:12 -08:00
});
});
}
2018-09-12 12:50:04 -07:00
function ensureCertificate(vhost, domain, auditSource, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof domain, 'string');
2018-01-30 15:16:34 -08:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
2018-11-14 19:36:12 -08:00
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
2018-11-14 19:36:12 -08:00
getCertApi(domainObject, function (error, api, apiOptions) {
if (error) return callback(error);
2018-11-14 20:38:49 -08:00
getCertificateByHostname(vhost, domainObject, function (error, currentBundle) {
if (currentBundle) {
debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`);
2015-12-11 22:25:22 -08:00
2018-11-14 20:38:49 -08:00
if (currentBundle.certFilePath.endsWith('.user.cert')) return callback(null, currentBundle); // user certs cannot be renewed
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle);
2018-11-14 19:36:12 -08:00
debug(`ensureCertificate: ${vhost} cert require renewal`);
} else {
debug(`ensureCertificate: ${vhost} cert does not exist`);
}
2018-11-14 19:36:12 -08:00
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
2018-02-02 21:21:51 -08:00
2018-11-14 19:36:12 -08:00
api.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) {
var errorMessage = error ? error.message : '';
2018-01-30 15:16:34 -08:00
2018-11-14 19:36:12 -08:00
if (error) {
debug('ensureCertificate: could not get certificate. using fallback certs', error);
mailer.certificateRenewalError(vhost, errorMessage);
}
2018-11-14 20:38:49 -08:00
eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: errorMessage });
2018-11-14 19:36:12 -08:00
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
if (!certFilePath || !keyFilePath) return getFallbackCertificate(domain, callback);
callback(null, { certFilePath, keyFilePath, type: 'new-le' });
});
});
});
});
}
function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof callback, 'function');
var data = {
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: config.adminOrigin(),
vhost: vhost, // if vhost is empty it will become the default_server
hasIPv6: config.hasIPv6(),
endpoint: 'admin',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
xFrameOptions: 'SAMEORIGIN',
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
if (vhost) safe.fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf')); // remove legacy admin.conf. remove after 3.5
reload(callback);
}
function configureAdmin(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
2018-01-30 19:59:09 -08:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
const adminFqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
ensureCertificate(adminFqdn, domainObject.domain, auditSource, function (error, bundle) {
if (error) return callback(error);
writeAdminNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
});
});
}
function writeAdminConfig(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
const adminFqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
2018-12-19 14:20:48 -08:00
getCertificate(adminFqdn, domainObject.domain, function (error, bundle) {
if (error) return callback(error);
writeAdminNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
});
});
}
function writeAppNginxConfig(app, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
2018-02-08 15:07:49 +01:00
vhost: app.fqdn,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
2018-02-08 15:07:49 +01:00
debug('writing config for "%s" to %s with options %j', app.fqdn, nginxConfigFilename, data);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
2018-02-08 15:07:49 +01:00
debug('Error creating nginx config for "%s" : %s', app.fqdn, safe.error.message);
return callback(safe.error);
}
reload(callback);
}
function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
2018-06-29 16:14:13 +02:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof callback, 'function');
var data = {
sourceDir: path.resolve(__dirname, '..'),
vhost: fqdn,
redirectTo: app.fqdn,
hasIPv6: config.hasIPv6(),
endpoint: 'redirect',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: null,
xFrameOptions: 'SAMEORIGIN'
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
// if we change the filename, also change it in unconfigureApp()
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}-redirect-${fqdn}.conf`);
2018-06-29 16:14:13 +02:00
debug('writing config for "%s" redirecting to "%s" to %s with options %j', app.fqdn, fqdn, nginxConfigFilename, data);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx redirect config for "%s" : %s', app.fqdn, safe.error.message);
return callback(safe.error);
}
reload(callback);
}
2018-01-30 15:16:34 -08:00
function configureApp(app, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
2018-01-30 15:16:34 -08:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
2018-09-12 12:50:04 -07:00
ensureCertificate(app.fqdn, app.domain, auditSource, function (error, bundle) {
if (error) return callback(error);
writeAppNginxConfig(app, bundle, function (error) {
if (error) return callback(error);
async.eachSeries(app.alternateDomains, function (alternateDomain, callback) {
ensureCertificate(alternateDomain.fqdn, alternateDomain.domain, auditSource, function (error, bundle) {
if (error) return callback(error);
2018-06-29 16:14:13 +02:00
writeAppRedirectNginxConfig(app, alternateDomain.fqdn, bundle, callback);
});
}, callback);
});
});
}
function unconfigureApp(app, callback) {
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);
reload(callback);
});
}
2018-12-10 20:20:53 -08:00
function renewCerts(options, auditSource, progressCallback, callback) {
2018-10-24 13:01:45 -07:00
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
2018-12-10 20:20:53 -08:00
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
2018-09-12 12:50:04 -07:00
var appDomains = [];
// add webadmin domain
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${config.adminFqdn()}.conf`) });
// add app main
allApps.forEach(function (app) {
appDomains.push({ domain: app.domain, fqdn: app.fqdn, type: 'main', app: app, nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf') });
app.alternateDomains.forEach(function (alternateDomain) {
let nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}-redirect-${alternateDomain.fqdn}.conf`);
appDomains.push({ domain: alternateDomain.domain, fqdn: alternateDomain.fqdn, type: 'alternate', app: app, nginxConfigFilename: nginxConfigFilename });
});
});
2018-10-24 13:01:45 -07:00
if (options.domain) appDomains = appDomains.filter(function (appDomain) { return appDomain.domain === options.domain; });
2018-12-10 20:20:53 -08:00
let progress = 1;
2018-09-12 12:50:04 -07:00
async.eachSeries(appDomains, function (appDomain, iteratorCallback) {
2018-12-10 20:20:53 -08:00
progressCallback({ percent: progress, message: `Renewing certs of ${appDomain.fqdn}` });
progress += Math.round(100/appDomains.length);
2018-09-12 12:50:04 -07:00
ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle) {
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
2018-11-14 19:36:12 -08:00
// 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();
2018-10-24 20:06:43 -07:00
debug(`renewCerts: creating new nginx config since ${appDomain.nginxConfigFilename} does not have ${bundle.certFilePath}`);
// reconfigure since the cert changed
var configureFunc;
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${config.adminFqdn()}.conf`, config.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppNginxConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectNginxConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
2018-12-11 10:49:04 -08:00
else return iteratorCallback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
configureFunc(function (ignoredError) {
if (ignoredError) debug('renewAll: error reconfiguring app', ignoredError);
2018-12-11 11:02:32 -08:00
platform.handleCertChanged(appDomain.fqdn, iteratorCallback);
});
});
2018-12-11 10:49:04 -08:00
}, callback);
});
}
2018-10-24 13:01:45 -07:00
function renewAll(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('renewAll: Checking certificates for renewal');
renewCerts({}, auditSource, callback);
}
function removeAppConfigs() {
for (let appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && !appConfigFile.startsWith(constants.ADMIN_LOCATION)) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}
}
function configureDefaultServer(callback) {
assert.strictEqual(typeof callback, 'function');
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
debug('configureDefaultServer: create new cert');
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=${cn} -nodes`)) {
debug(`configureDefaultServer: could not generate certificate: ${safe.error.message}`);
return callback(safe.error);
}
}
writeAdminNginxConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
callback(null);
});
}