merge certificates.js and nginx.js to reverseproxy.js

when certs change, we have to call into nginx anyway. since they
go hand in hand, just merge those files. modern reverse proxies
do this job integrated already.
This commit is contained in:
Girish Ramakrishnan
2018-01-30 12:23:27 -08:00
parent 8e63d63509
commit f379724128
12 changed files with 209 additions and 224 deletions
+3 -3
View File
@@ -54,7 +54,6 @@ var addons = require('./addons.js'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = backups.BackupsError,
certificates = require('./certificates.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
@@ -71,6 +70,7 @@ var addons = require('./addons.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
spawn = require('child_process').spawn,
@@ -535,7 +535,7 @@ function install(data, auditSource, callback) {
if (error) return callback(error);
if (cert && key) {
error = certificates.validateCertificate(intrinsicFqdn, cert, key);
error = reverseProxy.validateCertificate(intrinsicFqdn, cert, key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
@@ -654,7 +654,7 @@ function configure(appId, data, auditSource, callback) {
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
error = certificates.validateCertificate(intrinsicFqdn, data.cert, data.key);
error = reverseProxy.validateCertificate(intrinsicFqdn, data.cert, data.key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
+16 -17
View File
@@ -8,8 +8,8 @@ exports = module.exports = {
// exported for testing
_reserveHttpPort: reserveHttpPort,
_configureNginx: configureNginx,
_unconfigureNginx: unconfigureNginx,
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_createVolume: createVolume,
_deleteVolume: deleteVolume,
_verifyManifest: verifyManifest,
@@ -32,7 +32,6 @@ var addons = require('./addons.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
config = require('./config.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
@@ -44,10 +43,10 @@ var addons = require('./addons.js'),
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
net = require('net'),
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
superagent = require('superagent'),
@@ -113,23 +112,23 @@ function reserveHttpPort(app, callback) {
});
}
function configureNginx(app, callback) {
function configureReverseProxy(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
certificates.ensureCertificate(app, function (error, certFilePath, keyFilePath) {
reverseProxy.ensureCertificate(app, function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
nginx.configureApp(app, certFilePath, keyFilePath, callback);
reverseProxy.configureApp(app, certFilePath, keyFilePath, callback);
});
}
function unconfigureNginx(app, callback) {
function unconfigureReverseProxy(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// TODO: maybe revoke the cert
nginx.unconfigureApp(app, callback);
reverseProxy.unconfigureApp(app, callback);
}
function createContainer(app, callback) {
@@ -393,7 +392,7 @@ function install(app, callback) {
// teardown for re-installs
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureNginx.bind(null, app),
unconfigureReverseProxy.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
@@ -454,8 +453,8 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
configureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
// done!
function (callback) {
@@ -503,7 +502,7 @@ function configure(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureNginx.bind(null, app),
unconfigureReverseProxy.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
@@ -549,8 +548,8 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
configureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
// done!
function (callback) {
@@ -709,8 +708,8 @@ function uninstall(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
removeIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring Nginx' }),
unconfigureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring reverse proxy' }),
unconfigureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
appdb.del.bind(null, app.id)
+2 -2
View File
@@ -28,12 +28,12 @@ var assert = require('assert'),
eventlog = require('./eventlog.js'),
locker = require('./locker.js'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
progress = require('./progress.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
@@ -81,7 +81,7 @@ function initialize(callback) {
async.series([
settings.initialize,
nginx.configureDefaultServer,
reverseProxy.configureDefaultServer,
cron.initialize, // required for caas heartbeat before activation
onActivated
], callback);
+2 -2
View File
@@ -10,7 +10,6 @@ var apps = require('./apps.js'),
assert = require('assert'),
backups = require('./backups.js'),
caas = require('./caas.js'),
certificates = require('./certificates.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
constants = require('./constants.js'),
@@ -20,6 +19,7 @@ var apps = require('./apps.js'),
dyndns = require('./dyndns.js'),
eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'),
reverseProxy = require('./reverseproxy.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
semver = require('semver'),
@@ -175,7 +175,7 @@ function recreateJobs(tz) {
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
gJobs.certificateRenew = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: certificates.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
onTick: reverseProxy.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
+9 -8
View File
@@ -22,12 +22,12 @@ module.exports = exports = {
var assert = require('assert'),
caas = require('./caas.js'),
config = require('./config.js'),
certificates = require('./certificates.js'),
CertificatesError = certificates.CertificatesError,
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
safe = require('safetydance'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
@@ -115,7 +115,7 @@ function add(domain, zoneName, provider, config, fallbackCertificate, callback)
}
if (fallbackCertificate) {
let error = certificates.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
let error = reverseProxy.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
}
@@ -134,7 +134,7 @@ function add(domain, zoneName, provider, config, fallbackCertificate, callback)
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainError(DomainError.ALREADY_EXISTS));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
certificates.setFallbackCertificate(domain, fallbackCertificate, function (error) {
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
callback();
@@ -153,8 +153,8 @@ 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));
certificates.getFallbackCertificate(domain, function (error, certFilePath, keyFilePath) {
if (error && error.reason !== CertificatesError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
reverseProxy.getFallbackCertificate(domain, function (error, certFilePath, keyFilePath) {
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
var cert = safe.fs.readFileSync(certFilePath, 'utf-8');
var key = safe.fs.readFileSync(keyFilePath, 'utf-8');
@@ -190,7 +190,7 @@ function update(domain, provider, config, fallbackCertificate, callback) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (fallbackCertificate) {
let error = certificates.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
let error = reverseProxy.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
}
@@ -211,8 +211,9 @@ function update(domain, provider, config, fallbackCertificate, callback) {
if (!fallbackCertificate) return callback();
certificates.setFallbackCertificate(domain, fallbackCertificate, function (error) {
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
callback();
});
});
+2 -2
View File
@@ -38,7 +38,6 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
certificates = require('./certificates.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
@@ -56,6 +55,7 @@ var assert = require('assert'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
smtpTransport = require('nodemailer-smtp-transport'),
@@ -524,7 +524,7 @@ function restartMail(callback) {
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
// admin and mail share the same certificate
certificates.getCertificate({ intrinsicFqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, cert, key) {
reverseProxy.getCertificate({ intrinsicFqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, cert, key) {
if (error) return callback(error);
// the setup script copies dhparams.pem to /addons/mail
-137
View File
@@ -1,137 +0,0 @@
'use strict';
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:nginx'),
ejs = require('ejs'),
fs = require('fs'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
util = require('util');
exports = module.exports = {
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
reload: reload,
removeAppConfigs: removeAppConfigs,
configureDefaultServer: configureDefaultServer
};
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function configureAdmin(certFilePath, keyFilePath, configFileName, vhost, callback) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
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: certFilePath,
keyFilePath: 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);
reload(callback);
}
function configureApp(app, certFilePath, keyFilePath, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
var vhost = app.altDomain || app.intrinsicFqdn;
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
certFilePath: certFilePath,
keyFilePath: 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');
debug('writing config for "%s" to %s with options %j', vhost, nginxConfigFilename, data);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx config for "%s" : %s', vhost, safe.error.message);
return callback(safe.error);
}
reload(callback);
}
function unconfigureApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || app.intrinsicFqdn;
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', vhost, safe.error.message);
return callback(null);
}
reload(callback);
}
function reload(callback) {
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
}
function removeAppConfigs() {
for (var appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}
function configureDefaultServer(callback) {
callback = callback || NOOP_CALLBACK;
if (process.env.BOX_ENV === 'test') return callback();
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
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
safe.child_process.execSync(certCommand);
}
configureAdmin(certFilePath, keyFilePath, 'default.conf', '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
callback(null);
});
}
+2 -2
View File
@@ -17,9 +17,9 @@ var apps = require('./apps.js'),
infra = require('./infra_version.js'),
locker = require('./locker.js'),
mail = require('./mail.js'),
nginx = require('./nginx.js'),
os = require('os'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
shell = require('./shell.js'),
@@ -265,7 +265,7 @@ function startApps(existingInfra, callback) {
apps.restoreInstalledApps(callback);
} else {
debug('startApps: reconfiguring installed apps');
nginx.removeAppConfigs(); // should we change the cert location, nginx will not start
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
apps.configureInstalledApps(callback);
}
}
+145 -22
View File
@@ -1,7 +1,7 @@
'use strict';
exports = module.exports = {
CertificatesError: CertificatesError,
ReverseProxyError: ReverseProxyError,
setFallbackCertificate: setFallbackCertificate,
getFallbackCertificate: getFallbackCertificate,
@@ -13,6 +13,13 @@ exports = module.exports = {
renewAll: renewAll,
configureDefaultServer: configureDefaultServer,
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
reload: reload,
removeAppConfigs: removeAppConfigs,
// exported for testing
_getApi: getApi
};
@@ -25,20 +32,25 @@ var acme = require('./cert/acme.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:certificates'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
user = require('./user.js'),
util = require('util');
function CertificatesError(reason, errorOrMessage) {
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
NOOP_CALLBACK = function (error) { if (error) debug(error); };
function ReverseProxyError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -56,10 +68,10 @@ function CertificatesError(reason, errorOrMessage) {
this.nestedError = errorOrMessage;
}
}
util.inherits(CertificatesError, Error);
CertificatesError.INTERNAL_ERROR = 'Internal Error';
CertificatesError.INVALID_CERT = 'Invalid certificate';
CertificatesError.NOT_FOUND = 'Not Found';
util.inherits(ReverseProxyError, Error);
ReverseProxyError.INTERNAL_ERROR = 'Internal Error';
ReverseProxyError.INVALID_CERT = 'Invalid certificate';
ReverseProxyError.NOT_FOUND = 'Not Found';
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
@@ -178,8 +190,8 @@ function renewAll(auditSource, callback) {
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
var configureFunc = app.intrinsicFqdn === config.adminFqdn() ?
nginx.configureAdmin.bind(null, certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
configureAdmin.bind(null, certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: configureApp.bind(null, app, certFilePath, keyFilePath);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
@@ -210,11 +222,11 @@ function validateCertificate(domain, cert, key) {
}
// check for empty cert and key strings
if (!cert && key) return new CertificatesError(CertificatesError.INVALID_CERT, 'missing cert');
if (cert && !key) return new CertificatesError(CertificatesError.INVALID_CERT, 'missing key');
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
var result = safe.child_process.execSync('openssl x509 -noout -checkhost "' + domain + '"', { encoding: 'utf8', input: cert });
if (!result) return new CertificatesError(CertificatesError.INVALID_CERT, 'Unable to get certificate subject.');
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
// if no match, check alt names
if (result.indexOf('does match certificate') === -1) {
@@ -227,17 +239,17 @@ function validateCertificate(domain, cert, key) {
debug('validateCertificate: detected altNames as %j', altNames);
// check altNames
if (!altNames.some(matchesDomain)) return new CertificatesError(CertificatesError.INVALID_CERT, util.format('Certificate is not valid for this domain. Expecting %s in %j', domain, altNames));
if (!altNames.some(matchesDomain)) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, util.format('Certificate is not valid for this domain. Expecting %s in %j', domain, 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 CertificatesError(CertificatesError.INVALID_CERT, 'Key does not match the certificate.');
if (certModulus !== keyModulus) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, '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 CertificatesError(CertificatesError.INVALID_CERT, 'Certificate has expired.');
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Certificate has expired.');
return null;
}
@@ -254,24 +266,24 @@ function setFallbackCertificate(domain, fallback, callback) {
if (fallback) {
// backup the cert
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
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));
} else if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=*.%s -nodes', keyFilePath, certFilePath, domain);
if (!safe.child_process.execSync(certCommand)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.child_process.execSync(certCommand)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
}
// copy over fallback cert
var fallbackCertFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
var fallbackKeyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
if (!safe.child_process.execSync(`cp ${certFilePath} ${fallbackCertFilePath}`)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.child_process.execSync(`cp ${keyFilePath} ${fallbackKeyFilePath}`)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.child_process.execSync(`cp ${certFilePath} ${fallbackCertFilePath}`)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.child_process.execSync(`cp ${keyFilePath} ${fallbackKeyFilePath}`)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
platform.handleCertChanged('*.' + domain);
nginx.reload(function (error) {
if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error));
reload(function (error) {
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
return callback(null);
});
@@ -348,3 +360,114 @@ function ensureCertificate(app, callback) {
});
});
}
function configureAdmin(certFilePath, keyFilePath, configFileName, vhost, callback) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
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: certFilePath,
keyFilePath: 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);
reload(callback);
}
function configureApp(app, certFilePath, keyFilePath, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
var vhost = app.altDomain || app.intrinsicFqdn;
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
certFilePath: certFilePath,
keyFilePath: 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');
debug('writing config for "%s" to %s with options %j', vhost, nginxConfigFilename, data);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx config for "%s" : %s', vhost, safe.error.message);
return callback(safe.error);
}
reload(callback);
}
function unconfigureApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || app.intrinsicFqdn;
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', vhost, safe.error.message);
return callback(null);
}
reload(callback);
}
function reload(callback) {
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
}
function removeAppConfigs() {
for (var appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}
function configureDefaultServer(callback) {
callback = callback || NOOP_CALLBACK;
if (process.env.BOX_ENV === 'test') return callback();
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
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
safe.child_process.execSync(certCommand);
}
configureAdmin(certFilePath, keyFilePath, 'default.conf', '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
callback(null);
});
}
+3 -4
View File
@@ -15,7 +15,6 @@ var assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
certificates = require('./certificates.js'),
config = require('./config.js'),
constants = require('./constants.js'),
clients = require('./clients.js'),
@@ -26,9 +25,9 @@ var assert = require('assert'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
nginx = require('./nginx.js'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settingsdb = require('./settingsdb.js'),
@@ -130,12 +129,12 @@ function configureWebadmin(callback) {
function configureNginx(error) {
debug('configureNginx: dns update: %j', error || {});
certificates.ensureCertificate({ domain: config.adminDomain(), location: config.adminLocation(), intrinsicFqdn: config.adminFqdn() }, function (error, certFilePath, keyFilePath) {
reverseProxy.ensureCertificate({ domain: config.adminDomain(), location: config.adminLocation(), intrinsicFqdn: config.adminFqdn() }, function (error, certFilePath, keyFilePath) {
if (error) return done(error);
gWebadminStatus.tls = true;
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
reverseProxy.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
});
}
+2 -2
View File
@@ -133,7 +133,7 @@ describe('apptask', function () {
});
it('configure nginx correctly', function (done) {
apptask._configureNginx(APP, function (error) {
apptask._configureReverseProxy(APP, function (error) {
expect(fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + APP.id + '.conf'));
// expect(error).to.be(null); // this fails because nginx cannot be restarted
done();
@@ -141,7 +141,7 @@ describe('apptask', function () {
});
it('unconfigure nginx', function (done) {
apptask._unconfigureNginx(APP, function (error) {
apptask._unconfigureReverseProxy(APP, function (error) {
expect(!fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + APP.id + '.conf'));
// expect(error).to.be(null); // this fails because nginx cannot be restarted
done();
@@ -6,9 +6,9 @@
'use strict';
var async = require('async'),
certificates = require('../certificates.js'),
database = require('../database.js'),
expect = require('expect.js'),
reverseProxy = require('../reverseproxy.js'),
settings = require('../settings.js');
function setup(done) {
@@ -48,48 +48,48 @@ describe('Certificates', function () {
var validKey2 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBPQIBAAJBALSqMkz639g4ym51u169R20b1fqrh03BplKuWpwyOxuMP2m6g1xm\nMmpBx5T8mcWKexVkMQpvN6x1Lg09S4iyAWUCAwEAAQJBAJXu7YHPbjfuoalcUZzF\nbuKRCFtZQRf5z0Os6QvZ8A3iR0SzYJzx+c2ibp7WdifMXp3XaKm4tHSOfumrjUIq\nt10CIQDrs9Xo7bq0zuNjUV5IshNfaiYKZRfQciRVW2O8xBP9VwIhAMQ5CCEDZy+u\nsaF9RtmB0bjbe6XonBlAzoflfH/MAwWjAiEA50hL+ohr0MfCMM7DKaozgEj0kvan\n645VQLywnaX5x3kCIQDCwjinS9FnKmV0e/uOd6PJb0/S5IXLKt/TUpu33K5DMQIh\nAM9peu3B5t9pO59MmeUGZwI+bEJfEb+h03WTptBxS3pO\n-----END RSA PRIVATE KEY-----';
it('does not allow empty string for cert', function () {
expect(certificates.validateCertificate('foobar.com', '', 'key')).to.be.an(Error);
expect(reverseProxy.validateCertificate('foobar.com', '', 'key')).to.be.an(Error);
});
it('does not allow empty string for key', function () {
expect(certificates.validateCertificate('foobar.com', 'cert', '')).to.be.an(Error);
expect(reverseProxy.validateCertificate('foobar.com', 'cert', '')).to.be.an(Error);
});
it('does not allow invalid cert', function () {
expect(certificates.validateCertificate('foobar.com', 'someinvalidcert', validKey0)).to.be.an(Error);
expect(reverseProxy.validateCertificate('foobar.com', 'someinvalidcert', validKey0)).to.be.an(Error);
});
it('does not allow invalid key', function () {
expect(certificates.validateCertificate('foobar.com', validCert0, 'invalidkey')).to.be.an(Error);
expect(reverseProxy.validateCertificate('foobar.com', validCert0, 'invalidkey')).to.be.an(Error);
});
it('does not allow cert without matching domain', function () {
expect(certificates.validateCertificate('cloudron.io', validCert0, validKey0)).to.be.an(Error);
expect(reverseProxy.validateCertificate('cloudron.io', validCert0, validKey0)).to.be.an(Error);
});
it('allows valid cert with matching domain', function () {
expect(certificates.validateCertificate('foobar.com', validCert0, validKey0)).to.be(null);
expect(reverseProxy.validateCertificate('foobar.com', validCert0, validKey0)).to.be(null);
});
it('allows valid cert with matching domain (wildcard)', function () {
expect(certificates.validateCertificate('abc.foobar.com', validCert1, validKey1)).to.be(null);
expect(reverseProxy.validateCertificate('abc.foobar.com', validCert1, validKey1)).to.be(null);
});
it('does now allow cert without matching domain (wildcard)', function () {
expect(certificates.validateCertificate('foobar.com', validCert1, validKey1)).to.be.an(Error);
expect(certificates.validateCertificate('bar.abc.foobar.com', validCert1, validKey1)).to.be.an(Error);
expect(reverseProxy.validateCertificate('foobar.com', validCert1, validKey1)).to.be.an(Error);
expect(reverseProxy.validateCertificate('bar.abc.foobar.com', validCert1, validKey1)).to.be.an(Error);
});
it('allows valid cert with matching domain (subdomain)', function () {
expect(certificates.validateCertificate('baz.foobar.com', validCert2, validKey2)).to.be(null);
expect(reverseProxy.validateCertificate('baz.foobar.com', validCert2, validKey2)).to.be(null);
});
it('does not allow cert without matching domain (subdomain)', function () {
expect(certificates.validateCertificate('baz.foobar.com', validCert0, validKey0)).to.be.an(Error);
expect(reverseProxy.validateCertificate('baz.foobar.com', validCert0, validKey0)).to.be.an(Error);
});
it('does not allow invalid cert/key tuple', function () {
expect(certificates.validateCertificate('foobar.com', validCert0, validKey1)).to.be.an(Error);
expect(reverseProxy.validateCertificate('foobar.com', validCert0, validKey1)).to.be.an(Error);
});
});
@@ -104,7 +104,7 @@ describe('Certificates', function () {
after(cleanup);
it('returns prod caas for prod cloudron', function (done) {
certificates._getApi({ }, function (error, api, options) {
reverseProxy._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('caas');
expect(options.prod).to.be(true);
@@ -113,7 +113,7 @@ describe('Certificates', function () {
});
it('returns prod caas for dev cloudron', function (done) {
certificates._getApi({ }, function (error, api, options) {
reverseProxy._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('caas');
expect(options.prod).to.be(true);
@@ -122,7 +122,7 @@ describe('Certificates', function () {
});
it('returns prod-acme with altDomain in prod cloudron', function (done) {
certificates._getApi({ altDomain: 'foo.something.com' }, function (error, api, options) {
reverseProxy._getApi({ altDomain: 'foo.something.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -131,7 +131,7 @@ describe('Certificates', function () {
});
it('returns prod acme with altDomain in dev cloudron', function (done) {
certificates._getApi({ altDomain: 'foo.something.com' }, function (error, api, options) {
reverseProxy._getApi({ altDomain: 'foo.something.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -151,7 +151,7 @@ describe('Certificates', function () {
after(cleanup);
it('returns prod acme in prod cloudron', function (done) {
certificates._getApi({ }, function (error, api, options) {
reverseProxy._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -160,7 +160,7 @@ describe('Certificates', function () {
});
it('returns prod acme with altDomain in prod cloudron', function (done) {
certificates._getApi({ altDomain: 'foo.bar.com' }, function (error, api, options) {
reverseProxy._getApi({ altDomain: 'foo.bar.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -169,7 +169,7 @@ describe('Certificates', function () {
});
it('returns prod acme in dev cloudron', function (done) {
certificates._getApi({ }, function (error, api, options) {
reverseProxy._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -189,7 +189,7 @@ describe('Certificates', function () {
after(cleanup);
it('returns staging acme in prod cloudron', function (done) {
certificates._getApi({ }, function (error, api, options) {
reverseProxy._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
@@ -198,7 +198,7 @@ describe('Certificates', function () {
});
it('returns staging acme in dev cloudron', function (done) {
certificates._getApi({ }, function (error, api, options) {
reverseProxy._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
@@ -207,7 +207,7 @@ describe('Certificates', function () {
});
it('returns staging acme with altDomain in prod cloudron', function (done) {
certificates._getApi({ altDomain: 'foo.bar.com' }, function (error, api, options) {
reverseProxy._getApi({ altDomain: 'foo.bar.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);