diff --git a/src/apps.js b/src/apps.js index ef09cb8b6..fb3cd24ec 100644 --- a/src/apps.js +++ b/src/apps.js @@ -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)); diff --git a/src/apptask.js b/src/apptask.js index e1d6dfa4e..051b3798f 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -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) diff --git a/src/cloudron.js b/src/cloudron.js index e6956466a..1eefa3a43 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -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); diff --git a/src/cron.js b/src/cron.js index 36760238d..653e548c5 100644 --- a/src/cron.js +++ b/src/cron.js @@ -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 }); diff --git a/src/domains.js b/src/domains.js index 1259f52d6..69b3d0129 100644 --- a/src/domains.js +++ b/src/domains.js @@ -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(); }); }); diff --git a/src/mail.js b/src/mail.js index 73d647169..dd91e2f5b 100644 --- a/src/mail.js +++ b/src/mail.js @@ -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 diff --git a/src/nginx.js b/src/nginx.js deleted file mode 100644 index 3b0464441..000000000 --- a/src/nginx.js +++ /dev/null @@ -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); - }); -} diff --git a/src/platform.js b/src/platform.js index a2bd0bbb0..9b955f7ac 100644 --- a/src/platform.js +++ b/src/platform.js @@ -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); } } diff --git a/src/certificates.js b/src/reverseproxy.js similarity index 69% rename from src/certificates.js rename to src/reverseproxy.js index 4567fdd9d..7e2c270ba 100644 --- a/src/certificates.js +++ b/src/reverseproxy.js @@ -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); + }); +} diff --git a/src/setup.js b/src/setup.js index 9f3e83158..d1bb560a5 100644 --- a/src/setup.js +++ b/src/setup.js @@ -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); }); } diff --git a/src/test/apptask-test.js b/src/test/apptask-test.js index b2088d548..41b0e0406 100644 --- a/src/test/apptask-test.js +++ b/src/test/apptask-test.js @@ -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(); diff --git a/src/test/certificates-test.js b/src/test/reverseproxy-test.js similarity index 87% rename from src/test/certificates-test.js rename to src/test/reverseproxy-test.js index a13372ed2..32cb542eb 100644 --- a/src/test/certificates-test.js +++ b/src/test/reverseproxy-test.js @@ -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);