Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89bff16053 | |||
| a89476c538 | |||
| f51b61e407 | |||
| 177103bccd | |||
| f31d63aabd | |||
| fd20246e8b | |||
| 0c1ea39a02 | |||
| a409dd026d | |||
| 4731f8e5a7 | |||
| 7e05259b0e | |||
| 14ab85dc4f | |||
| 0651bfc4b8 | |||
| 21b94b2655 | |||
| 4e40c2341a | |||
| d9a83eacd2 | |||
| 7b40674c0d | |||
| 936c1989f1 | |||
| cfe336c37c | |||
| d8a1e4aab0 | |||
| be4d2afff3 | |||
| c2a4ef5f93 | |||
| 22634b4ceb | |||
| abc4975b3d | |||
| 36d81ff8d1 | |||
| fe94190c2f | |||
| f32027e15b | |||
| 4b6a92955b | |||
| 35a2da744c | |||
| 9d91340223 | |||
| e0a56f75c3 | |||
| 4cfd30f9e8 | |||
| 3fbcbf0e5d | |||
| 8b7833e8b1 | |||
| 66441f133d | |||
| 8a12d6019a | |||
| 39c626dc75 | |||
| a7480c3f29 | |||
| 8af682acf1 | |||
| 95eba1db81 | |||
| 0b8fde7d8d | |||
| 2f7517152a | |||
| 3e2ea0e087 | |||
| 723556d6a2 | |||
| 1f53d76cef | |||
| d15488431b | |||
| cf80fd7dc5 | |||
| 73d891b98e | |||
| 875ec1028d | |||
| fd985c2011 | |||
| 47981004c9 | |||
| e3f7c8f63d | |||
| 853db53f82 | |||
| 5992c0534a | |||
| 1874c93c5c | |||
| 3c4adb1aed | |||
| 66db918273 | |||
| 69845d5ddd | |||
| 42181d597b | |||
| b56e9ca745 | |||
| 5fc4788269 | |||
| d0f8293b73 | |||
| 44582bcd4b | |||
| 5c73aed953 | |||
| e1ec48530e | |||
| 54c4053728 | |||
| 79ffb0df5c | |||
| c510952c88 | |||
| 6109da531d | |||
| 56877332db | |||
| 6fc972d160 | |||
| 5346153d9b | |||
| aaf266d272 | |||
| 0750db9aae | |||
| 316976d295 | |||
| 593b5d945b | |||
| 88f0240757 | |||
| f5c2f8849d | |||
| 5c4a8f7803 | |||
| 5b8fdad5cb | |||
| fe819f95ec | |||
| be6728f8cb | |||
| 24d3a81bc8 | |||
| 268c7b5bcf | |||
| 64716a2de5 | |||
| d2c8457ab1 | |||
| 667cb84af7 | |||
| df8653cdd5 | |||
| 32f677ca0d |
@@ -14,9 +14,9 @@ var appHealthMonitor = require('./src/apphealthmonitor.js'),
|
||||
async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
simpleauth = require('./src/simpleauth.js'),
|
||||
oauthproxy = require('./src/oauthproxy.js'),
|
||||
server = require('./src/server.js');
|
||||
server = require('./src/server.js'),
|
||||
simpleauth = require('./src/simpleauth.js');
|
||||
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
@@ -26,7 +26,6 @@ console.log();
|
||||
console.log(' Environment: ', config.CLOUDRON ? 'CLOUDRON' : 'TEST');
|
||||
console.log(' Version: ', config.version());
|
||||
console.log(' Admin Origin: ', config.adminOrigin());
|
||||
console.log(' Appstore token: ', config.token());
|
||||
console.log(' Appstore API server origin: ', config.apiServerOrigin());
|
||||
console.log(' Appstore Web server origin: ', config.webServerOrigin());
|
||||
console.log();
|
||||
|
||||
Generated
+2194
-1619
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -14,6 +14,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"async": "^1.2.1",
|
||||
"attempt": "^1.0.1",
|
||||
"aws-sdk": "^2.1.46",
|
||||
"body-parser": "^1.13.1",
|
||||
"bytes": "^2.1.0",
|
||||
@@ -53,7 +54,7 @@
|
||||
"passport-oauth2-client-password": "^0.1.2",
|
||||
"password-generator": "^1.0.0",
|
||||
"proxy-middleware": "^0.13.0",
|
||||
"safetydance": "0.0.19",
|
||||
"safetydance": "^0.1.0",
|
||||
"semver": "^4.3.6",
|
||||
"serve-favicon": "^2.2.0",
|
||||
"split": "^1.0.0",
|
||||
@@ -61,6 +62,7 @@
|
||||
"supererror": "^0.7.1",
|
||||
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
|
||||
"underscore": "^1.7.0",
|
||||
"ursa": "^0.9.1",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^3.30.0",
|
||||
"x509": "^0.2.2"
|
||||
|
||||
@@ -11,6 +11,7 @@ arg_is_custom_domain="false"
|
||||
arg_restore_key=""
|
||||
arg_restore_url=""
|
||||
arg_retire="false"
|
||||
arg_tls_config=""
|
||||
arg_tls_cert=""
|
||||
arg_tls_key=""
|
||||
arg_token=""
|
||||
@@ -37,6 +38,9 @@ EOF
|
||||
arg_tls_cert=$(echo "$2" | $json tlsCert)
|
||||
arg_tls_key=$(echo "$2" | $json tlsKey)
|
||||
|
||||
arg_tls_config=$(echo "$2" | $json tlsConfig)
|
||||
[[ "${arg_tls_config}" == "null" ]] && arg_tls_config=""
|
||||
|
||||
arg_restore_url=$(echo "$2" | $json restore.url)
|
||||
[[ "${arg_restore_url}" == "null" ]] && arg_restore_url=""
|
||||
|
||||
@@ -66,5 +70,6 @@ echo "restore url: ${arg_restore_url}"
|
||||
echo "tls cert: ${arg_tls_cert}"
|
||||
echo "tls key: ${arg_tls_key}"
|
||||
echo "token: ${arg_token}"
|
||||
echo "tlsConfig: ${arg_tls_config}"
|
||||
echo "version: ${arg_version}"
|
||||
echo "web server: ${arg_web_server_origin}"
|
||||
|
||||
@@ -14,4 +14,6 @@ User=yellowtent
|
||||
Group=yellowtent
|
||||
MemoryLimit=200M
|
||||
TimeoutStopSec=5s
|
||||
StartLimitInterval=1
|
||||
StartLimitBurst=60
|
||||
|
||||
|
||||
+16
-11
@@ -48,6 +48,7 @@ mkdir -p "${DATA_DIR}/mongodb"
|
||||
mkdir -p "${DATA_DIR}/snapshots"
|
||||
mkdir -p "${DATA_DIR}/addons"
|
||||
mkdir -p "${DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${DATA_DIR}/acme"
|
||||
|
||||
# bookkeep the version as part of data
|
||||
echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_versions_url}\" }" > "${DATA_DIR}/box/version"
|
||||
@@ -97,20 +98,16 @@ ln -sfF "df-disk_by-uuid_${vda1_id}" "${DATA_DIR}/graphite/whisper/collectd/loca
|
||||
service collectd restart
|
||||
|
||||
set_progress "30" "Setup nginx"
|
||||
# setup naked domain to use admin by default. app restoration will overwrite this config
|
||||
mkdir -p "${DATA_DIR}/nginx/applications"
|
||||
cp "${script_dir}/start/nginx/nginx.conf" "${DATA_DIR}/nginx/nginx.conf"
|
||||
cp "${script_dir}/start/nginx/mime.types" "${DATA_DIR}/nginx/mime.types"
|
||||
|
||||
# generate the main nginx config file
|
||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/nginx.ejs" \
|
||||
-O "{ \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/nginx.conf"
|
||||
|
||||
# generate these for update code paths as well to overwrite splash
|
||||
admin_cert_file="cert/host.cert"
|
||||
admin_key_file="cert/host.key"
|
||||
if [[ -f "${DATA_DIR}/box/certs/admin.cert" && -f "${DATA_DIR}/box/certs/admin.key" ]]; then
|
||||
admin_cert_file="${DATA_DIR}/box/certs/admin.cert"
|
||||
admin_key_file="${DATA_DIR}/box/certs/admin.key"
|
||||
admin_cert_file="${DATA_DIR}/nginx/cert/host.cert"
|
||||
admin_key_file="${DATA_DIR}/nginx/cert/host.key"
|
||||
if [[ -f "${DATA_DIR}/box/certs/${admin_fqdn}.cert" && -f "${DATA_DIR}/box/certs/${admin_fqdn}.key" ]]; then
|
||||
admin_cert_file="${DATA_DIR}/box/certs/${admin_fqdn}.cert"
|
||||
admin_key_file="${DATA_DIR}/box/certs/${admin_fqdn}.key"
|
||||
fi
|
||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"${admin_cert_file}\", \"keyFilePath\": \"${admin_key_file}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
@@ -125,7 +122,7 @@ else
|
||||
fi
|
||||
|
||||
set_progress "33" "Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons"
|
||||
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
|
||||
chown "${USER}:${USER}" "${DATA_DIR}"
|
||||
|
||||
set_progress "40" "Setting up infra"
|
||||
@@ -179,6 +176,14 @@ if [[ ! -z "${arg_dns_config}" ]]; then
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
|
||||
fi
|
||||
|
||||
# Add TLS Configuration
|
||||
if [[ ! -z "${arg_tls_config}" ]]; then
|
||||
echo "Add TLS Config"
|
||||
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
|
||||
fi
|
||||
|
||||
# Add webadmin oauth client
|
||||
# The domain might have changed, therefor we have to update the record
|
||||
# !!! This needs to be in sync with the webadmin, specifically login_callback.js
|
||||
|
||||
@@ -38,6 +38,12 @@ http {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# acme challenges
|
||||
location /.well-known/acme-challenge/ {
|
||||
default_type text/plain;
|
||||
alias /home/yellowtent/data/acme/;
|
||||
}
|
||||
|
||||
location / {
|
||||
# redirect everything to HTTPS
|
||||
return 301 https://$host$request_uri;
|
||||
@@ -55,7 +61,7 @@ http {
|
||||
error_page 404 = @fallback;
|
||||
location @fallback {
|
||||
internal;
|
||||
root <%= sourceDir %>/webadmin/dist;
|
||||
root /home/yellowtent/box/webadmin/dist;
|
||||
rewrite ^/$ /nakeddomain.html break;
|
||||
}
|
||||
|
||||
+3
-5
@@ -48,6 +48,7 @@ var addons = require('./addons.js'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
certificates = require('./certificates.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
@@ -59,7 +60,6 @@ var addons = require('./addons.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
semver = require('semver'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
@@ -340,7 +340,7 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
}
|
||||
}
|
||||
|
||||
error = settings.validateCertificate(cert, key, config.appFqdn(location));
|
||||
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
debug('Will install app with id : ' + appId);
|
||||
@@ -381,7 +381,7 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
|
||||
error = validateAccessRestriction(accessRestriction);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
error = settings.validateCertificate(cert, key, config.appFqdn(location));
|
||||
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
@@ -497,8 +497,6 @@ function getLogs(appId, lines, follow, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState)));
|
||||
|
||||
var args = [ '--output=json', '--no-pager', '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
args = args.concat(appLogFilter(app));
|
||||
|
||||
+34
-62
@@ -9,7 +9,7 @@ exports = module.exports = {
|
||||
startTask: startTask,
|
||||
|
||||
// exported for testing
|
||||
_getFreePort: getFreePort,
|
||||
_reserveHttpPort: reserveHttpPort,
|
||||
_configureNginx: configureNginx,
|
||||
_unconfigureNginx: unconfigureNginx,
|
||||
_createVolume: createVolume,
|
||||
@@ -19,7 +19,6 @@ exports = module.exports = {
|
||||
_verifyManifest: verifyManifest,
|
||||
_registerSubdomain: registerSubdomain,
|
||||
_unregisterSubdomain: unregisterSubdomain,
|
||||
_reloadNginx: reloadNginx,
|
||||
_waitForDnsPropagation: waitForDnsPropagation
|
||||
};
|
||||
|
||||
@@ -36,6 +35,7 @@ var addons = require('./addons.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
certificates = require('./certificates.js'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
config = require('./config.js'),
|
||||
database = require('./database.js'),
|
||||
@@ -47,6 +47,7 @@ var addons = require('./addons.js'),
|
||||
hat = require('hat'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
net = require('net'),
|
||||
nginx = require('./nginx.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
@@ -59,9 +60,7 @@ var addons = require('./addons.js'),
|
||||
uuid = require('node-uuid'),
|
||||
_ = require('underscore');
|
||||
|
||||
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
|
||||
COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
|
||||
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_COLLECTD_CMD = path.join(__dirname, 'scripts/reloadcollectd.sh'),
|
||||
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
|
||||
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
|
||||
@@ -77,66 +76,34 @@ function debugApp(app, args) {
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
// We expect conflicts to not happen despite closing the port (parallel app installs, app update does not reconfigure nginx etc)
|
||||
// https://tools.ietf.org/html/rfc6056#section-3.5 says linux uses random ephemeral port allocation
|
||||
function getFreePort(callback) {
|
||||
function reserveHttpPort(app, callback) {
|
||||
var server = net.createServer();
|
||||
server.listen(0, function () {
|
||||
var port = server.address().port;
|
||||
server.close(function () {
|
||||
return callback(null, port);
|
||||
updateApp(app, { httpPort: port }, function (error) {
|
||||
if (error) {
|
||||
server.close();
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
server.close(callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reloadNginx(callback) {
|
||||
shell.sudo('reloadNginx', [ RELOAD_NGINX_CMD ], callback);
|
||||
}
|
||||
function configureNginx(app, callback) {
|
||||
var vhost = config.appFqdn(app.location);
|
||||
|
||||
function configureNginx(app, callback) {
|
||||
getFreePort(function (error, freePort) {
|
||||
certificates.ensureCertificate(vhost, function (error, certFilePath, keyFilePath) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var sourceDir = path.resolve(__dirname, '..');
|
||||
var endpoint = app.oauthProxy ? 'oauthproxy' : 'app';
|
||||
var vhost = config.appFqdn(app.location);
|
||||
var certFilePath = safe.fs.statSync(path.join(paths.APP_CERTS_DIR, vhost + '.cert')) ? path.join(paths.APP_CERTS_DIR, vhost + '.cert') : 'cert/host.cert';
|
||||
var keyFilePath = safe.fs.statSync(path.join(paths.APP_CERTS_DIR, vhost + '.key')) ? path.join(paths.APP_CERTS_DIR, vhost + '.key') : 'cert/host.key';
|
||||
|
||||
var data = {
|
||||
sourceDir: sourceDir,
|
||||
adminOrigin: config.adminOrigin(),
|
||||
vhost: vhost,
|
||||
port: freePort,
|
||||
endpoint: endpoint,
|
||||
certFilePath: certFilePath,
|
||||
keyFilePath: keyFilePath
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
debugApp(app, 'writing config to %s', nginxConfigFilename);
|
||||
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
|
||||
debugApp(app, 'Error creating nginx config : %s', safe.error.message);
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
async.series([
|
||||
exports._reloadNginx,
|
||||
updateApp.bind(null, app, { httpPort: freePort })
|
||||
], callback);
|
||||
nginx.configureApp(app, certFilePath, keyFilePath, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function unconfigureNginx(app, callback) {
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
|
||||
debugApp(app, 'Error removing nginx configuration : %s', safe.error.message);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
exports._reloadNginx(callback);
|
||||
// TODO: maybe revoke the cert
|
||||
nginx.unconfigureApp(app, callback);
|
||||
}
|
||||
|
||||
function createContainer(app, callback) {
|
||||
@@ -343,6 +310,7 @@ function install(app, callback) {
|
||||
|
||||
// teardown for re-installs
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
@@ -351,10 +319,8 @@ function install(app, callback) {
|
||||
unregisterSubdomain.bind(null, app, app.location),
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
// removeIcon.bind(null, app), // do not remove icon for non-appstore installs
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '15, Configure nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
@@ -385,6 +351,9 @@ function install(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'installed');
|
||||
@@ -429,6 +398,7 @@ function restore(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
@@ -442,10 +412,8 @@ function restore(app, callback) {
|
||||
},
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
removeIcon.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '30, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
@@ -476,6 +444,9 @@ function restore(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'restored');
|
||||
@@ -495,6 +466,7 @@ function restore(app, callback) {
|
||||
function configure(app, callback) {
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
@@ -504,10 +476,8 @@ function configure(app, callback) {
|
||||
unregisterSubdomain(app, app.oldConfig.location, next);
|
||||
},
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '25, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '30, Create OAuth proxy credentials' }),
|
||||
allocateOAuthProxyCredentials.bind(null, app),
|
||||
@@ -530,6 +500,9 @@ function configure(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'configured');
|
||||
@@ -660,7 +633,7 @@ function stopApp(app, callback) {
|
||||
docker.stopContainers(app.id, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
updateApp(app, { runState: appdb.RSTATE_STOPPED }, callback);
|
||||
updateApp(app, { runState: appdb.RSTATE_STOPPED, health: null }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -723,4 +696,3 @@ if (require.main === module) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('../config.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
ursa = require('ursa'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
|
||||
CA_STAGING = 'https://acme-staging.api.letsencrypt.org/',
|
||||
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate
|
||||
};
|
||||
|
||||
function AcmeError(reason, errorOrMessage) {
|
||||
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(AcmeError, Error);
|
||||
AcmeError.INTERNAL_ERROR = 'Internal Error';
|
||||
AcmeError.EXTERNAL_ERROR = 'External Error';
|
||||
AcmeError.ALREADY_EXISTS = 'Already Exists';
|
||||
AcmeError.NOT_COMPLETED = 'Not Completed';
|
||||
AcmeError.FORBIDDEN = 'Forbidden';
|
||||
|
||||
// http://jose.readthedocs.org/en/latest/
|
||||
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
|
||||
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
|
||||
|
||||
function getNonce(callback) {
|
||||
superagent.get(CA_STAGING + '/directory', function (error, response) {
|
||||
if (error) return callback(error);
|
||||
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
|
||||
|
||||
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
|
||||
});
|
||||
}
|
||||
|
||||
// urlsafe base64 encoding (jose)
|
||||
function urlBase64Encode(string) {
|
||||
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
}
|
||||
|
||||
function b64(str) {
|
||||
var buf = util.isBuffer(str) ? str : new Buffer(str);
|
||||
return urlBase64Encode(buf.toString('base64'));
|
||||
}
|
||||
|
||||
function sendSignedRequest(url, accountKeyPem, payload, callback) {
|
||||
assert.strictEqual(typeof url, 'string');
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof payload, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var privateKey = ursa.createPrivateKey(accountKeyPem);
|
||||
|
||||
var header = {
|
||||
alg: 'RS256',
|
||||
jwk: {
|
||||
e: b64(privateKey.getExponent()),
|
||||
kty: 'RSA',
|
||||
n: b64(privateKey.getModulus())
|
||||
}
|
||||
};
|
||||
|
||||
var payload64 = b64(payload);
|
||||
|
||||
getNonce(function (error, nonce) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('Using nonce %s', nonce);
|
||||
|
||||
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
|
||||
|
||||
var signer = ursa.createSigner('sha256');
|
||||
signer.update(protected64 + '.' + payload64, 'utf8');
|
||||
var signature64 = urlBase64Encode(signer.sign(privateKey, 'base64'));
|
||||
|
||||
var data = {
|
||||
header: header,
|
||||
protected: protected64,
|
||||
payload: payload64,
|
||||
signature: signature64
|
||||
};
|
||||
|
||||
superagent.post(url).set('Content-Type', 'application/x-www-form-urlencoded').send(JSON.stringify(data)).end(function (error, res) {
|
||||
if (error && !error.response) return callback(error); // network errors
|
||||
|
||||
callback(null, res);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function registerUser(accountKeyPem, email, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
resource: 'new-reg',
|
||||
contact: [ 'mailto:' + email ],
|
||||
agreement: LE_AGREEMENT
|
||||
};
|
||||
|
||||
debug('registerUser: %s', email);
|
||||
|
||||
sendSignedRequest(CA_STAGING + '/acme/new-reg', accountKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode === 409) return callback(new AcmeError(AcmeError.ALREADY_EXISTS, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('registerUser: registered user %s', email);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function registerDomain(accountKeyPem, domain, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
resource: 'new-authz',
|
||||
identifier: {
|
||||
type: 'dns',
|
||||
value: domain
|
||||
}
|
||||
};
|
||||
|
||||
debug('registerDomain: %s', domain);
|
||||
|
||||
sendSignedRequest(CA_STAGING + '/acme/new-authz', accountKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new AcmeError(AcmeError.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('registerDomain: registered %s', domain);
|
||||
|
||||
callback(null, result.body);
|
||||
});
|
||||
}
|
||||
|
||||
function prepareHttpChallenge(accountKeyPem, challenge, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
|
||||
|
||||
var token = challenge.token;
|
||||
|
||||
var privateKey = ursa.createPrivateKey(accountKeyPem);
|
||||
|
||||
var jwk = {
|
||||
e: b64(privateKey.getExponent()),
|
||||
kty: 'RSA',
|
||||
n: b64(privateKey.getModulus())
|
||||
};
|
||||
|
||||
var shasum = crypto.createHash('sha256');
|
||||
shasum.update(JSON.stringify(jwk));
|
||||
var thumbprint = urlBase64Encode(shasum.digest('base64'));
|
||||
var keyAuthorization = token + '.' + thumbprint;
|
||||
|
||||
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, token));
|
||||
|
||||
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, token), token + '.' + thumbprint, function (error) {
|
||||
if (error) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function notifyChallengeReady(accountKeyPem, challenge, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('notifyChallengeReady: %s was met', challenge.uri);
|
||||
|
||||
var keyAuthorization = fs.readFileSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), 'utf8');
|
||||
|
||||
var payload = {
|
||||
resource: 'challenge',
|
||||
keyAuthorization: keyAuthorization
|
||||
};
|
||||
|
||||
sendSignedRequest(challenge.uri, accountKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function waitForChallenge(challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('waitingForChallenge: %j', challenge);
|
||||
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
debug('waitingForChallenge: getting status');
|
||||
|
||||
superagent.get(challenge.uri).end(function (error, result) {
|
||||
if (error && !error.response) {
|
||||
debug('waitForChallenge: network error getting uri %s', challenge.uri);
|
||||
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, error.message)); // network error
|
||||
}
|
||||
if (result.statusCode !== 202) {
|
||||
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForChallenge: status is "%s"', result.body.status);
|
||||
|
||||
if (result.body.status === 'pending') return retryCallback(new AcmeError(AcmeError.NOT_COMPLETED));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
else return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
});
|
||||
}, function retryFinished(error) {
|
||||
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
|
||||
function signCertificate(accountKeyPem, domain, csrDer, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(util.isBuffer(csrDer));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
|
||||
var payload = {
|
||||
resource: 'new-cert',
|
||||
csr: b64(csrDer)
|
||||
};
|
||||
|
||||
debug('signCertificate: sending new-cert request');
|
||||
|
||||
sendSignedRequest(CA_STAGING + '/acme/new-cert', accountKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var certUrl = result.headers.location;
|
||||
|
||||
if (!certUrl) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
|
||||
|
||||
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // for renewal
|
||||
|
||||
return callback(null, result.headers.location);
|
||||
});
|
||||
}
|
||||
|
||||
function createKeyAndCsr(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
var execSync = safe.child_process.execSync;
|
||||
|
||||
var privateKeyFile = path.join(outdir, domain + '.key');
|
||||
var key = execSync('openssl genrsa 4096');
|
||||
if (!key) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
|
||||
|
||||
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
|
||||
if (!csrDer) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
var csrFile = path.join(outdir, domain + '.csr');
|
||||
if (!safe.fs.writeFileSync(csrFile, csrFile)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
|
||||
|
||||
callback(null, csrDer);
|
||||
}
|
||||
|
||||
function downloadCertificate(domain, certUrl, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof certUrl, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
|
||||
superagent.get(certUrl).buffer().parse(function (res, done) {
|
||||
var data = [ ];
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
}).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode === 202) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, 'Retry not implemented yet'));
|
||||
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var certificateDer = result.text;
|
||||
var execSync = safe.child_process.execSync;
|
||||
|
||||
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
|
||||
debug('downloadCertificate: cert der file saved');
|
||||
|
||||
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
|
||||
if (!certificatePem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
var chainPem = safe.fs.readFileSync(__dirname + '/lets-encrypt-x1-cross-signed.pem.txt');
|
||||
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
var certificateFile = path.join(outdir, domain + '.cert');
|
||||
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('downloadCertificate: cert file saved at %s', certificateFile);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function acmeFlow(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var email = 'admin@' + config.fqdn();
|
||||
var accountKeyPem;
|
||||
|
||||
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
|
||||
debug('getCertificate: generating acme account key on first run');
|
||||
accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!accountKeyPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, accountKeyPem);
|
||||
} else {
|
||||
accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
|
||||
}
|
||||
|
||||
registerUser(accountKeyPem, email, function (error) {
|
||||
if (error && error.reason !== AcmeError.ALREADY_EXISTS) return callback(error);
|
||||
|
||||
registerDomain(accountKeyPem, domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('acmeFlow: challenges: %j', result);
|
||||
|
||||
var httpChallenges = result.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
if (httpChallenges.length === 0) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'no http challenges'));
|
||||
var challenge = httpChallenges[0];
|
||||
|
||||
async.waterfall([
|
||||
prepareHttpChallenge.bind(null, accountKeyPem, challenge),
|
||||
notifyChallengeReady.bind(null, accountKeyPem, challenge),
|
||||
waitForChallenge.bind(null, challenge),
|
||||
createKeyAndCsr.bind(null, domain),
|
||||
signCertificate.bind(null, accountKeyPem, domain),
|
||||
downloadCertificate.bind(null, domain)
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getCertificate(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
var certUrl = safe.fs.readFileSync(path.join(outdir, domain + '.url'), 'utf8');
|
||||
var certificateGetter;
|
||||
if (certUrl) {
|
||||
debug('getCertificate: renewing existing cert for %s from %s', domain, certUrl);
|
||||
certificateGetter = downloadCertificate.bind(null, domain, certUrl);
|
||||
} else {
|
||||
debug('getCertificate: start acme flow for %s', domain);
|
||||
certificateGetter = acmeFlow.bind(null, domain);
|
||||
}
|
||||
|
||||
certificateGetter(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:cert/caas.js');
|
||||
|
||||
function getCertificate(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getCertificate: using fallback certificate', domain);
|
||||
|
||||
return callback(null, 'cert/host.cert', 'cert/host.key');
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqDCCA5CgAwIBAgIRAJgT9HUT5XULQ+dDHpceRL0wDQYJKoZIhvcNAQELBQAw
|
||||
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
|
||||
Ew5EU1QgUm9vdCBDQSBYMzAeFw0xNTEwMTkyMjMzMzZaFw0yMDEwMTkyMjMzMzZa
|
||||
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
|
||||
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||
ggEPADCCAQoCggEBAJzTDPBa5S5Ht3JdN4OzaGMw6tc1Jhkl4b2+NfFwki+3uEtB
|
||||
BaupnjUIWOyxKsRohwuj43Xk5vOnYnG6eYFgH9eRmp/z0HhncchpDpWRz/7mmelg
|
||||
PEjMfspNdxIknUcbWuu57B43ABycrHunBerOSuu9QeU2mLnL/W08lmjfIypCkAyG
|
||||
dGfIf6WauFJhFBM/ZemCh8vb+g5W9oaJ84U/l4avsNwa72sNlRZ9xCugZbKZBDZ1
|
||||
gGusSvMbkEl4L6KWTyogJSkExnTA0DHNjzE4lRa6qDO4Q/GxH8Mwf6J5MRM9LTb4
|
||||
4/zyM2q5OTHFr8SNDR1kFjOq+oQpttQLwNh9w5MCAwEAAaOCAZIwggGOMBIGA1Ud
|
||||
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAy
|
||||
BggrBgEFBQcwAYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5j
|
||||
b20wOwYIKwYBBQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMv
|
||||
ZHN0cm9vdGNheDMucDdjMB8GA1UdIwQYMBaAFMSnsaR7LHH62+FLkHX/xBVghYkQ
|
||||
MFQGA1UdIARNMEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUH
|
||||
AgEWImh0dHA6Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUw
|
||||
MzAxoC+gLYYraHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JM
|
||||
LmNybDATBgNVHR4EDDAKoQgwBoIELm1pbDAdBgNVHQ4EFgQUqEpqYwR93brm0Tm3
|
||||
pkVl7/Oo7KEwDQYJKoZIhvcNAQELBQADggEBANHIIkus7+MJiZZQsY14cCoBG1hd
|
||||
v0J20/FyWo5ppnfjL78S2k4s2GLRJ7iD9ZDKErndvbNFGcsW+9kKK/TnY21hp4Dd
|
||||
ITv8S9ZYQ7oaoqs7HwhEMY9sibED4aXw09xrJZTC9zK1uIfW6t5dHQjuOWv+HHoW
|
||||
ZnupyxpsEUlEaFb+/SCI4KCSBdAsYxAcsHYI5xxEI4LutHp6s3OT2FuO90WfdsIk
|
||||
6q78OMSdn875bNjdBYAqxUp2/LEIHfDBkLoQz0hFJmwAbYahqKaLn73PAAm1X2kj
|
||||
f1w8DdnkabOLGeOVcj9LQ+s67vBykx4anTjURkbqZslUEUsn2k5xeua2zUk=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,231 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var acme = require('./cert/acme.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
caas = require('./cert/caas.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:src/certificates'),
|
||||
fs = require('fs'),
|
||||
nginx = require('./nginx.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js'),
|
||||
x509 = require('x509');
|
||||
|
||||
exports = module.exports = {
|
||||
installAdminCertificate: installAdminCertificate,
|
||||
autoRenew: autoRenew,
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
setAdminCertificate: setAdminCertificate,
|
||||
CertificatesError: CertificatesError,
|
||||
validateCertificate: validateCertificate,
|
||||
ensureCertificate: ensureCertificate
|
||||
};
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function CertificatesError(reason, errorOrMessage) {
|
||||
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(CertificatesError, Error);
|
||||
CertificatesError.INTERNAL_ERROR = 'Internal Error';
|
||||
CertificatesError.INVALID_CERT = 'Invalid certificate';
|
||||
|
||||
function getApi(callback) {
|
||||
settings.getTlsConfig(function (error, tlsConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var api = tlsConfig.provider === 'caas' ? caas : acme;
|
||||
|
||||
callback(null, api);
|
||||
});
|
||||
}
|
||||
|
||||
function installAdminCertificate(callback) {
|
||||
settings.getTlsConfig(function (error, tlsConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (tlsConfig.provider === 'caas') return callback();
|
||||
|
||||
waitForDns(config.adminFqdn(), sysinfo.getIp(), config.fqdn(), function (error) {
|
||||
if (error) return callback(error); // this cannot happen because we retry forever
|
||||
|
||||
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
|
||||
if (error) {
|
||||
debug('Error obtaining certificate. Proceed anyway', error);
|
||||
return callback();
|
||||
}
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function needsRenewalSync(certFilePath) {
|
||||
var result = safe.child_process.execSync('openssl x509 -checkend %s -in %s', 60 * 60 * 24 * 5, certFilePath);
|
||||
|
||||
return result === null; // command errored
|
||||
}
|
||||
|
||||
function autoRenew(callback) {
|
||||
debug('autoRenew: Checking certificates for renewal');
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var filenames = safe.fs.readdirSync(paths.APP_CERTS_DIR);
|
||||
if (!filenames) {
|
||||
debug('autoRenew: Error getting filenames: %s', safe.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
var certs = filenames.filter(function (f) {
|
||||
return f.match(/\.cert$/) !== null && needsRenewalSync(path.join(paths.APP_CERTS_DIR, f));
|
||||
});
|
||||
|
||||
debug('autoRenew: %j needs to be renewed', certs);
|
||||
|
||||
getApi(function (error, api) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(certs, function iterator(cert, iteratorCallback) {
|
||||
var domain = cert.match(/^(.*)\.cert$/)[1];
|
||||
if (domain === 'host') return iteratorCallback(); // cannot renew fallback cert
|
||||
|
||||
api.getCertificate(domain, function (error) {
|
||||
if (error) debug('autoRenew: could not renew cert for %s', domain, error);
|
||||
|
||||
iteratorCallback(); // move on to next cert
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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)
|
||||
function validateCertificate(cert, key, fqdn) {
|
||||
assert(cert === null || typeof cert === 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
|
||||
if (cert === null && key === null) return null;
|
||||
if (!cert && key) return new Error('missing cert');
|
||||
if (cert && !key) return new Error('missing key');
|
||||
|
||||
var content;
|
||||
try {
|
||||
content = x509.parseCert(cert);
|
||||
} catch (e) {
|
||||
return new Error('invalid cert: ' + e.message);
|
||||
}
|
||||
|
||||
// check expiration
|
||||
if (content.notAfter < new Date()) return new Error('cert expired');
|
||||
|
||||
function matchesDomain(domain) {
|
||||
if (domain === fqdn) return true;
|
||||
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// check domain
|
||||
var domains = content.altNames.concat(content.subject.commonName);
|
||||
if (!domains.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, domains));
|
||||
|
||||
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
|
||||
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
|
||||
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
|
||||
if (certModulus !== keyModulus) return new Error('key does not match the cert');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function setFallbackCertificate(cert, key, callback) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateCertificate(cert, key, '*.' + config.fqdn());
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
|
||||
|
||||
// backup the cert
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
// copy over fallback cert
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
nginx.reload(function (error) {
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function setAdminCertificate(cert, key, callback) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.appFqdn(constants.ADMIN_LOCATION);
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
|
||||
|
||||
var error = validateCertificate(cert, key, vhost);
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
|
||||
|
||||
// backup the cert
|
||||
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
||||
}
|
||||
|
||||
function ensureCertificate(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// check if user uploaded a specific cert. ideally, we should not mix user certs and automatic certs as we do here...
|
||||
var userCertFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
||||
var userKeyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
||||
|
||||
if (fs.existsSync(userCertFilePath) && fs.existsSync(userKeyFilePath)) {
|
||||
debug('ensureCertificate: %s. certificate already exists at %s', domain, userKeyFilePath);
|
||||
|
||||
if (!needsRenewalSync(userCertFilePath)) return callback(null, userCertFilePath, userKeyFilePath);
|
||||
|
||||
debug('ensureCertificate: %s cert require renewal', domain);
|
||||
}
|
||||
|
||||
getApi(function (error, api) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('ensureCertificate: getting certificate for %s', domain);
|
||||
|
||||
api.getCertificate(domain, callback);
|
||||
});
|
||||
}
|
||||
@@ -582,11 +582,6 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin()
|
||||
},
|
||||
tlsConfig: {
|
||||
provider: 'caas',
|
||||
cert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
|
||||
key: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
|
||||
},
|
||||
|
||||
version: boxUpdateInfo.version,
|
||||
boxVersionsUrl: config.get('boxVersionsUrl')
|
||||
|
||||
@@ -28,6 +28,7 @@ exports = module.exports = {
|
||||
// these values are derived
|
||||
adminOrigin: adminOrigin,
|
||||
internalAdminOrigin: internalAdminOrigin,
|
||||
adminFqdn: adminFqdn,
|
||||
appFqdn: appFqdn,
|
||||
zoneName: zoneName,
|
||||
|
||||
@@ -155,6 +156,10 @@ function appFqdn(location) {
|
||||
return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn();
|
||||
}
|
||||
|
||||
function adminFqdn() {
|
||||
return appFqdn(constants.ADMIN_LOCATION);
|
||||
}
|
||||
|
||||
function adminOrigin() {
|
||||
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
|
||||
}
|
||||
|
||||
+14
-1
@@ -7,6 +7,7 @@ exports = module.exports = {
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
certificates = require('./certificates.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
config = require('./config.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
@@ -23,7 +24,8 @@ var gAutoupdaterJob = null,
|
||||
gBackupJob = null,
|
||||
gCleanupTokensJob = null,
|
||||
gDockerVolumeCleanerJob = null,
|
||||
gSchedulerSyncJob = null;
|
||||
gSchedulerSyncJob = null,
|
||||
gCertificateRenewJob = null;
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
|
||||
@@ -107,6 +109,14 @@ function recreateJobs(unusedTimeZone, callback) {
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
});
|
||||
|
||||
if (gCertificateRenewJob) gCertificateRenewJob.stop();
|
||||
gCertificateRenewJob = new CronJob({
|
||||
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||
onTick: certificates.autoRenew,
|
||||
start: true,
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
});
|
||||
|
||||
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
|
||||
@@ -179,5 +189,8 @@ function uninitialize(callback) {
|
||||
if (gSchedulerSyncJob) gSchedulerSyncJob.stop();
|
||||
gSchedulerSyncJob = null;
|
||||
|
||||
if (gCertificateRenewJob) gCertificateRenewJob.stop();
|
||||
gCertificateRenewJob = null;
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -126,6 +126,8 @@ function clear(callback) {
|
||||
function beginTransaction(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gConnectionPool === null) return callback(new Error('No database connection pool.'));
|
||||
|
||||
gConnectionPool.getConnection(function (error, connection) {
|
||||
if (error) return callback(error);
|
||||
|
||||
|
||||
+7
-2
@@ -154,6 +154,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
}
|
||||
|
||||
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
|
||||
// for subcontainers, this should ideally be false. but docker does not allow network sharing if the app container is not running
|
||||
// this means cloudron exec does not work
|
||||
var isolatedNetworkNs = true;
|
||||
|
||||
addons.getEnvironment(app, function (error, addonEnv) {
|
||||
if (error) return callback(new Error('Error getting addon environment : ' + error));
|
||||
@@ -161,7 +164,8 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
var containerOptions = {
|
||||
name: name, // used for filtering logs
|
||||
// do _not_ set hostname to app fqdn. doing so sets up the dns name to look up the internal docker ip. this makes curl from within container fail
|
||||
Hostname: semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location),
|
||||
// for subcontainers, this should not be set because we already share the network namespace with app container
|
||||
Hostname: isolatedNetworkNs ? (semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location)) : null,
|
||||
Tty: isAppContainer,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: cmd,
|
||||
@@ -183,13 +187,14 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
PortBindings: isAppContainer ? dockerPortBindings : { },
|
||||
PublishAllPorts: false,
|
||||
ReadonlyRootfs: semver.gte(targetBoxVersion(app.manifest), '0.0.66'), // see also Volumes in startContainer
|
||||
Links: addons.getLinksSync(app, app.manifest.addons),
|
||||
RestartPolicy: {
|
||||
"Name": isAppContainer ? "always" : "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
CpuShares: 512, // relative to 1024 for system processes
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ":rw" ],
|
||||
NetworkMode: isolatedNetworkNs ? 'default' : ('container:' + app.containerId), // share network namespace with parent
|
||||
Links: isolatedNetworkNs ? addons.getLinksSync(app, app.manifest.addons) : null, // links is redundant with --net=container
|
||||
SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,8 +5,7 @@ Dear Admin,
|
||||
The application titled '<%= title %>' that you installed at <%= appFqdn %>
|
||||
is not responding.
|
||||
|
||||
This is most likely a problem in the application. Please report this issue to
|
||||
support@cloudron.io (by forwarding this email).
|
||||
This is most likely a problem in the application.
|
||||
|
||||
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
|
||||
|
||||
|
||||
+1
-1
@@ -284,7 +284,7 @@ function appDied(app) {
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: adminEmails.join(', '),
|
||||
to: adminEmails.concat('support@cloudron.io').join(', '),
|
||||
subject: util.format('App %s is down', app.location),
|
||||
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: config.appFqdn(app.location), format: 'text' })
|
||||
};
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:src/nginx'),
|
||||
ejs = require('ejs'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js');
|
||||
|
||||
exports = module.exports = {
|
||||
configureAdmin: configureAdmin,
|
||||
configureApp: configureApp,
|
||||
unconfigureApp: unconfigureApp,
|
||||
reload: reload
|
||||
};
|
||||
|
||||
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
|
||||
|
||||
function configureAdmin(certFilePath, keyFilePath, callback) {
|
||||
assert.strictEqual(typeof certFilePath, 'string');
|
||||
assert.strictEqual(typeof keyFilePath, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = {
|
||||
sourceDir: path.resolve(__dirname, '..'),
|
||||
adminOrigin: config.adminOrigin(),
|
||||
vhost: config.adminFqdn(),
|
||||
endpoint: 'admin',
|
||||
certFilePath: certFilePath,
|
||||
keyFilePath: keyFilePath
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf');
|
||||
|
||||
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.oauthProxy ? 'oauthproxy' : 'app';
|
||||
var vhost = config.appFqdn(app.location);
|
||||
|
||||
var data = {
|
||||
sourceDir: sourceDir,
|
||||
adminOrigin: config.adminOrigin(),
|
||||
vhost: vhost,
|
||||
port: app.httpPort,
|
||||
endpoint: endpoint,
|
||||
certFilePath: certFilePath,
|
||||
keyFilePath: keyFilePath
|
||||
};
|
||||
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', app.location, nginxConfigFilename);
|
||||
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
|
||||
debug('Error creating nginx config for "%s" : %s', app.location, safe.error.message);
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
function unconfigureApp(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
|
||||
debug('Error removing nginx configuration of "%s": %s', app.location, safe.error.message);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
function reload(callback) {
|
||||
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
|
||||
}
|
||||
+4
-1
@@ -28,5 +28,8 @@ exports = module.exports = {
|
||||
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'),
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json')
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json'),
|
||||
|
||||
ACME_CHALLENGES_DIR: path.join(config.baseDir(), 'data/acme'),
|
||||
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'data/box/acme/acme.key')
|
||||
};
|
||||
|
||||
@@ -23,6 +23,8 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
certificates = require('../certificates.js'),
|
||||
CertificatesError = require('../certificates.js').CertificatesError,
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
safe = require('safetydance'),
|
||||
@@ -142,8 +144,8 @@ function setCertificate(req, res, next) {
|
||||
if (!req.body.cert || typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
|
||||
settings.setCertificate(req.body.cert, req.body.key, function (error) {
|
||||
if (error && error.reason === SettingsError.INVALID_CERT) return next(new HttpError(400, error.message));
|
||||
certificates.setFallbackCertificate(req.body.cert, req.body.key, function (error) {
|
||||
if (error && error.reason === CertificatesError.INVALID_CERT) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
@@ -157,8 +159,8 @@ function setAdminCertificate(req, res, next) {
|
||||
if (!req.body.cert || typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
|
||||
settings.setAdminCertificate(req.body.cert, req.body.key, function (error) {
|
||||
if (error && error.reason === SettingsError.INVALID_CERT) return next(new HttpError(400, error.message));
|
||||
certificates.setAdminCertificate(req.body.cert, req.body.key, function (error) {
|
||||
if (error && error.reason === CertificatesError.INVALID_CERT) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
|
||||
@@ -156,6 +156,7 @@ function setup(done) {
|
||||
},
|
||||
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
|
||||
settings.setTlsConfig.bind(null, { provider: 'caas' }),
|
||||
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
|
||||
], done);
|
||||
}
|
||||
@@ -1064,6 +1065,8 @@ describe('App installation - port bindings', function () {
|
||||
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
|
||||
|
||||
settings.setTlsConfig.bind(null, { provider: 'caas' }),
|
||||
|
||||
function (callback) {
|
||||
awsHockInstance
|
||||
.get('/2013-04-01/hostedzone')
|
||||
|
||||
@@ -10,6 +10,7 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
auth = require('./auth.js'),
|
||||
certificates = require('./certificates.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
cron = require('./cron.js'),
|
||||
config = require('./config.js'),
|
||||
@@ -234,6 +235,7 @@ function start(callback) {
|
||||
async.series([
|
||||
auth.initialize,
|
||||
database.initialize,
|
||||
certificates.installAdminCertificate,
|
||||
cloudron.initialize, // keep this here because it reads activation state that others depend on
|
||||
taskmanager.initialize,
|
||||
mailer.initialize,
|
||||
|
||||
+33
-115
@@ -26,37 +26,31 @@ exports = module.exports = {
|
||||
getBackupConfig: getBackupConfig,
|
||||
setBackupConfig: setBackupConfig,
|
||||
|
||||
getTlsConfig: getTlsConfig,
|
||||
setTlsConfig: setTlsConfig,
|
||||
|
||||
getDefaultSync: getDefaultSync,
|
||||
getAll: getAll,
|
||||
|
||||
validateCertificate: validateCertificate,
|
||||
setCertificate: setCertificate,
|
||||
setAdminCertificate: setAdminCertificate,
|
||||
|
||||
AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern',
|
||||
TIME_ZONE_KEY: 'time_zone',
|
||||
CLOUDRON_NAME_KEY: 'cloudron_name',
|
||||
DEVELOPER_MODE_KEY: 'developer_mode',
|
||||
DNS_CONFIG_KEY: 'dns_config',
|
||||
BACKUP_CONFIG_KEY: 'backup_config',
|
||||
TLS_CONFIG_KEY: 'tls_config',
|
||||
|
||||
events: new (require('events').EventEmitter)()
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
ejs = require('ejs'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settingsdb = require('./settingsdb.js'),
|
||||
shell = require('./shell.js'),
|
||||
util = require('util'),
|
||||
x509 = require('x509'),
|
||||
_ = require('underscore');
|
||||
|
||||
var gDefaults = (function () {
|
||||
@@ -67,13 +61,11 @@ var gDefaults = (function () {
|
||||
result[exports.DEVELOPER_MODE_KEY] = false;
|
||||
result[exports.DNS_CONFIG_KEY] = { };
|
||||
result[exports.BACKUP_CONFIG_KEY] = { };
|
||||
result[exports.TLS_CONFIG_KEY] = { provider: 'caas' };
|
||||
|
||||
return result;
|
||||
})();
|
||||
|
||||
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
|
||||
|
||||
if (config.TEST) {
|
||||
// avoid noisy warnings during npm test
|
||||
exports.events.setMaxListeners(100);
|
||||
@@ -101,7 +93,6 @@ util.inherits(SettingsError, Error);
|
||||
SettingsError.INTERNAL_ERROR = 'Internal Error';
|
||||
SettingsError.NOT_FOUND = 'Not Found';
|
||||
SettingsError.BAD_FIELD = 'Bad Field';
|
||||
SettingsError.INVALID_CERT = 'Invalid certificate';
|
||||
|
||||
function setAutoupdatePattern(pattern, callback) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
@@ -276,6 +267,34 @@ function setDnsConfig(dnsConfig, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getTlsConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.TLS_CONFIG_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.TLS_CONFIG_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, JSON.parse(value)); // provider
|
||||
});
|
||||
}
|
||||
|
||||
function setTlsConfig(tlsConfig, callback) {
|
||||
assert.strictEqual(typeof tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('le-') !== 0) {
|
||||
return callback(new SettingsError(SettingsError.BAD_FIELD, 'provider must be caas or le-*'));
|
||||
}
|
||||
|
||||
settingsdb.set(exports.TLS_CONFIG_KEY, JSON.stringify(tlsConfig), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.TLS_CONFIG_KEY, tlsConfig);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getBackupConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -322,104 +341,3 @@ function getAll(callback) {
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
// 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)
|
||||
function validateCertificate(cert, key, fqdn) {
|
||||
assert(cert === null || typeof cert === 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
|
||||
if (cert === null && key === null) return null;
|
||||
if (!cert && key) return new Error('missing cert');
|
||||
if (cert && !key) return new Error('missing key');
|
||||
|
||||
var content;
|
||||
try {
|
||||
content = x509.parseCert(cert);
|
||||
} catch (e) {
|
||||
return new Error('invalid cert: ' + e.message);
|
||||
}
|
||||
|
||||
// check expiration
|
||||
if (content.notAfter < new Date()) return new Error('cert expired');
|
||||
|
||||
function matchesDomain(domain) {
|
||||
if (domain === fqdn) return true;
|
||||
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// check domain
|
||||
var domains = content.altNames.concat(content.subject.commonName);
|
||||
if (!domains.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, domains));
|
||||
|
||||
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
|
||||
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
|
||||
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
|
||||
if (certModulus !== keyModulus) return new Error('key does not match the cert');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function setCertificate(cert, key, callback) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateCertificate(cert, key, '*.' + config.fqdn());
|
||||
if (error) return callback(new SettingsError(SettingsError.INVALID_CERT, error.message));
|
||||
|
||||
// backup the cert
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.cert'), cert)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.key'), key)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
// copy over fallback cert
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), cert)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
shell.sudo('setCertificate', [ RELOAD_NGINX_CMD ], function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function setAdminCertificate(cert, key, callback) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var sourceDir = path.resolve(__dirname, '..');
|
||||
var endpoint = 'admin';
|
||||
var vhost = config.appFqdn(constants.ADMIN_LOCATION);
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, 'admin.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, 'admin.key');
|
||||
|
||||
var error = validateCertificate(cert, key, vhost);
|
||||
if (error) return callback(new SettingsError(SettingsError.INVALID_CERT, error.message));
|
||||
|
||||
// backup the cert
|
||||
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
var data = {
|
||||
sourceDir: sourceDir,
|
||||
adminOrigin: config.adminOrigin(),
|
||||
vhost: vhost,
|
||||
endpoint: endpoint,
|
||||
certFilePath: certFilePath,
|
||||
keyFilePath: keyFilePath
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf');
|
||||
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
|
||||
|
||||
shell.sudo('setAdminCertificate', [ RELOAD_NGINX_CMD ], function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,7 +85,8 @@ describe('apptask', function () {
|
||||
async.series([
|
||||
database.initialize,
|
||||
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.oauthProxy),
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' })
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
|
||||
settings.setTlsConfig.bind(null, { provider: 'caas' })
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -97,12 +98,12 @@ describe('apptask', function () {
|
||||
apptask.initialize(done);
|
||||
});
|
||||
|
||||
it('free port', function (done) {
|
||||
apptask._getFreePort(function (error, port) {
|
||||
expect(error).to.be(null);
|
||||
expect(port).to.be.a('number');
|
||||
var client = net.connect(port);
|
||||
client.on('connect', function () { done(new Error('Port is not free:' + port)); });
|
||||
it('reserve port', function (done) {
|
||||
apptask._reserveHttpPort(APP, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(APP.httpPort).to.be.a('number');
|
||||
var client = net.connect(APP.httpPort);
|
||||
client.on('connect', function () { done(new Error('Port is not free:' + APP.httpPort)); });
|
||||
client.on('error', function (error) { done(); });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var certificates = require('../certificates.js'),
|
||||
expect = require('expect.js');
|
||||
|
||||
describe('Certificates', function () {
|
||||
describe('validateCertificate', function () {
|
||||
/*
|
||||
Generate these with:
|
||||
openssl genrsa -out server.key 512
|
||||
openssl req -new -key server.key -out server.csr -subj "/C=DE/ST=Berlin/L=Berlin/O=Nebulon/OU=CTO/CN=baz.foobar.com"
|
||||
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
|
||||
*/
|
||||
|
||||
// foobar.com
|
||||
var validCert0 = '-----BEGIN CERTIFICATE-----\nMIIBujCCAWQCCQCjLyTKzAJ4FDANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzETMBEGA1UEAwwKZm9vYmFyLmNvbTAeFw0xNTEw\nMjgxMjM5MjZaFw0xNjEwMjcxMjM5MjZaMGQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQI\nDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UECgwHTmVidWxvbjEMMAoG\nA1UECwwDQ1RPMRMwEQYDVQQDDApmb29iYXIuY29tMFwwDQYJKoZIhvcNAQEBBQAD\nSwAwSAJBAMeYofgwHeNVmGkGe0gj4dnX2ciifDi7X2K/oVHp7mxuHjGMSYP9Z7b6\n+mu0IMf4OedwXStHBeO8mwjKxZmE7p8CAwEAATANBgkqhkiG9w0BAQsFAANBAJI7\nFUUHXjR63UFk8pgxp0c7hEGqj4VWWGsmo8oZnnX8jGVmQDKbk8o3MtDujfqupmMR\nMo7tSAFlG7zkm3GYhpw=\n-----END CERTIFICATE-----';
|
||||
var validKey0 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAMeYofgwHeNVmGkGe0gj4dnX2ciifDi7X2K/oVHp7mxuHjGMSYP9\nZ7b6+mu0IMf4OedwXStHBeO8mwjKxZmE7p8CAwEAAQJBAJS59Sb8o6i8JT9NJxvQ\nMQCkSJGqEaosZJ0uccSZ7aE48v+H7HiPzXAueitohcEif2Wp1EZ1RbRMURhznNiZ\neLECIQDxxqhakO6wc7H68zmpRXJ5ZxGUNbM24AMtpONAtEw9iwIhANNWtp6P74OV\ntvfOmtubbqw768fmGskFCOcp5oF8oF29AiBkTAf9AhCyjFwyAYJTEScq67HkLN66\njfVjkvpfFixmfwIgI+xldmZ5DCDyzQSthg7RrS0yUvRmMS1N6h1RNUl96PECIQDl\nit4lFcytbqNo1PuBZvzQE+plCjiJqXHYo3WCst1Jbg==\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
// *.foobar.com
|
||||
var validCert1 = '-----BEGIN CERTIFICATE-----\nMIIBvjCCAWgCCQCg957GWuHtbzANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEVMBMGA1UEAwwMKi5mb29iYXIuY29tMB4XDTE1\nMTAyODEzMDI1MFoXDTE2MTAyNzEzMDI1MFowZjELMAkGA1UEBhMCREUxDzANBgNV\nBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRAwDgYDVQQKDAdOZWJ1bG9uMQww\nCgYDVQQLDANDVE8xFTATBgNVBAMMDCouZm9vYmFyLmNvbTBcMA0GCSqGSIb3DQEB\nAQUAA0sAMEgCQQC0FKf07ZWMcABFlZw+GzXK9EiZrlJ1lpnu64RhN99z7MXRr8cF\nnZVgY3jgatuyR5s3WdzUvye2eJ0rNicl2EZJAgMBAAEwDQYJKoZIhvcNAQELBQAD\nQQAw4bteMZAeJWl2wgNLw+wTwAH96E0jyxwreCnT5AxJLmgimyQ0XOF4FsssdRFj\nxD9WA+rktelBodJyPeTDNhIh\n-----END CERTIFICATE-----';
|
||||
var validKey1 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBALQUp/TtlYxwAEWVnD4bNcr0SJmuUnWWme7rhGE333PsxdGvxwWd\nlWBjeOBq27JHmzdZ3NS/J7Z4nSs2JyXYRkkCAwEAAQJALV2eykcoC48TonQEPmkg\nbhaIS57syw67jMLsQImQ02UABKzqHPEKLXPOZhZPS9hsC/hGIehwiYCXMUlrl+WF\nAQIhAOntBI6qaecNjAAVG7UbZclMuHROUONmZUF1KNq6VyV5AiEAxRLkfHWy52CM\njOQrX347edZ30f4QczvugXwsyuU9A1ECIGlGZ8Sk4OBA8n6fAUcyO06qnmCJVlHg\npTUeOvKk5c9RAiBs28+8dCNbrbhVhx/yQr9FwNM0+ttJW/yWJ+pyNQhr0QIgJTT6\nxwCWYOtbioyt7B9l+ENy3AMSO3Uq+xmIKkvItK4=\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
// baz.foobar.com
|
||||
var validCert2 = '-----BEGIN CERTIFICATE-----\nMIIBwjCCAWwCCQDIKtL9RCDCkDANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wHhcN\nMTUxMDI4MTMwNTMzWhcNMTYxMDI3MTMwNTMzWjBoMQswCQYDVQQGEwJERTEPMA0G\nA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05lYnVsb24x\nDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEAw7UWW/VoQePv2l92l3XcntZeyw1nBiHxk1axZwC6auOW\n2/zfA//Tg7fv4q5qKnV1n/71IiMAheeFcpfogY5rTwIDAQABMA0GCSqGSIb3DQEB\nCwUAA0EAtluL6dGNfOdNkzoO/UwzRaIvEm2reuqe+Ik4WR/k+DJ4igrmRCQqXwjW\nJaGYsFWsuk3QLOWQ9YgCKlcIYd+1/A==\n-----END CERTIFICATE-----';
|
||||
var validKey2 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBAMO1Flv1aEHj79pfdpd13J7WXssNZwYh8ZNWsWcAumrjltv83wP/\n04O37+Kuaip1dZ/+9SIjAIXnhXKX6IGOa08CAwEAAQJAUPD3Y2cXDJFaJQXwhWnw\nqhzdLbvITUgCor5rNr+dWhE2MopGPpRHiabA1PeWEPx8CfblyTZGd8KUR/2W1c0r\naQIhAP4ZxB3+uhuzzMfyRrn/khr12pFn/FCIDbwnDbyUxLrTAiEAxSuVOFs+Mupt\nYCz/pPrDCx3eid0wyXRObbkLHOxJiBUCIBTp5fxaBNNW3xnt1OhmIo5Zgd3J4zh1\nmjvMMxM8Y1zFAiAxOP0qsZSoj1+41+MGY9fXaaCJ2F96m3+M4tpEYTTGNQIgdESZ\nz+hzHBeYVbWJpIR8uaNkx7wveUF90FpipXyeTsA=\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
it('allows both null', function () {
|
||||
expect(certificates.validateCertificate(null, null, 'foobar.com')).to.be(null);
|
||||
});
|
||||
|
||||
it('does not allow only cert', function () {
|
||||
expect(certificates.validateCertificate('cert', null, 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow only key', function () {
|
||||
expect(certificates.validateCertificate(null, 'key', 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow empty string for cert', function () {
|
||||
expect(certificates.validateCertificate('', 'key', 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow empty string for key', function () {
|
||||
expect(certificates.validateCertificate('cert', '', 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow invalid cert', function () {
|
||||
expect(certificates.validateCertificate('someinvalidcert', validKey0, 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow invalid key', function () {
|
||||
expect(certificates.validateCertificate(validCert0, 'invalidkey', 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow cert without matching domain', function () {
|
||||
expect(certificates.validateCertificate(validCert0, validKey0, 'cloudron.io')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain', function () {
|
||||
expect(certificates.validateCertificate(validCert0, validKey0, 'foobar.com')).to.be(null);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain (wildcard)', function () {
|
||||
expect(certificates.validateCertificate(validCert1, validKey1, 'abc.foobar.com')).to.be(null);
|
||||
});
|
||||
|
||||
it('does now allow cert without matching domain (wildcard)', function () {
|
||||
expect(certificates.validateCertificate(validCert1, validKey1, 'foobar.com')).to.be.an(Error);
|
||||
expect(certificates.validateCertificate(validCert1, validKey1, 'bar.abc.foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain (subdomain)', function () {
|
||||
expect(certificates.validateCertificate(validCert2, validKey2, 'baz.foobar.com')).to.be(null);
|
||||
});
|
||||
|
||||
it('does not allow cert without matching domain (subdomain)', function () {
|
||||
expect(certificates.validateCertificate(validCert0, validKey0, 'baz.foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow invalid cert/key tuple', function () {
|
||||
expect(certificates.validateCertificate(validCert0, validKey1, 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
+15
-81
@@ -100,6 +100,21 @@ describe('Settings', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('can set tls config', function (done) {
|
||||
settings.setTlsConfig({ provider: 'caas' }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get tls config', function (done) {
|
||||
settings.getTlsConfig(function (error, dnsConfig) {
|
||||
expect(error).to.be(null);
|
||||
expect(dnsConfig.provider).to.be('caas');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set backup config', function (done) {
|
||||
settings.setBackupConfig({ provider: 'caas', token: 'TOKEN' }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
@@ -126,85 +141,4 @@ describe('Settings', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCertificate', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
/*
|
||||
Generate these with:
|
||||
openssl genrsa -out server.key 512
|
||||
openssl req -new -key server.key -out server.csr -subj "/C=DE/ST=Berlin/L=Berlin/O=Nebulon/OU=CTO/CN=baz.foobar.com"
|
||||
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
|
||||
*/
|
||||
|
||||
// foobar.com
|
||||
var validCert0 = '-----BEGIN CERTIFICATE-----\nMIIBujCCAWQCCQCjLyTKzAJ4FDANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzETMBEGA1UEAwwKZm9vYmFyLmNvbTAeFw0xNTEw\nMjgxMjM5MjZaFw0xNjEwMjcxMjM5MjZaMGQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQI\nDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UECgwHTmVidWxvbjEMMAoG\nA1UECwwDQ1RPMRMwEQYDVQQDDApmb29iYXIuY29tMFwwDQYJKoZIhvcNAQEBBQAD\nSwAwSAJBAMeYofgwHeNVmGkGe0gj4dnX2ciifDi7X2K/oVHp7mxuHjGMSYP9Z7b6\n+mu0IMf4OedwXStHBeO8mwjKxZmE7p8CAwEAATANBgkqhkiG9w0BAQsFAANBAJI7\nFUUHXjR63UFk8pgxp0c7hEGqj4VWWGsmo8oZnnX8jGVmQDKbk8o3MtDujfqupmMR\nMo7tSAFlG7zkm3GYhpw=\n-----END CERTIFICATE-----';
|
||||
var validKey0 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAMeYofgwHeNVmGkGe0gj4dnX2ciifDi7X2K/oVHp7mxuHjGMSYP9\nZ7b6+mu0IMf4OedwXStHBeO8mwjKxZmE7p8CAwEAAQJBAJS59Sb8o6i8JT9NJxvQ\nMQCkSJGqEaosZJ0uccSZ7aE48v+H7HiPzXAueitohcEif2Wp1EZ1RbRMURhznNiZ\neLECIQDxxqhakO6wc7H68zmpRXJ5ZxGUNbM24AMtpONAtEw9iwIhANNWtp6P74OV\ntvfOmtubbqw768fmGskFCOcp5oF8oF29AiBkTAf9AhCyjFwyAYJTEScq67HkLN66\njfVjkvpfFixmfwIgI+xldmZ5DCDyzQSthg7RrS0yUvRmMS1N6h1RNUl96PECIQDl\nit4lFcytbqNo1PuBZvzQE+plCjiJqXHYo3WCst1Jbg==\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
// *.foobar.com
|
||||
var validCert1 = '-----BEGIN CERTIFICATE-----\nMIIBvjCCAWgCCQCg957GWuHtbzANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEVMBMGA1UEAwwMKi5mb29iYXIuY29tMB4XDTE1\nMTAyODEzMDI1MFoXDTE2MTAyNzEzMDI1MFowZjELMAkGA1UEBhMCREUxDzANBgNV\nBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRAwDgYDVQQKDAdOZWJ1bG9uMQww\nCgYDVQQLDANDVE8xFTATBgNVBAMMDCouZm9vYmFyLmNvbTBcMA0GCSqGSIb3DQEB\nAQUAA0sAMEgCQQC0FKf07ZWMcABFlZw+GzXK9EiZrlJ1lpnu64RhN99z7MXRr8cF\nnZVgY3jgatuyR5s3WdzUvye2eJ0rNicl2EZJAgMBAAEwDQYJKoZIhvcNAQELBQAD\nQQAw4bteMZAeJWl2wgNLw+wTwAH96E0jyxwreCnT5AxJLmgimyQ0XOF4FsssdRFj\nxD9WA+rktelBodJyPeTDNhIh\n-----END CERTIFICATE-----';
|
||||
var validKey1 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBALQUp/TtlYxwAEWVnD4bNcr0SJmuUnWWme7rhGE333PsxdGvxwWd\nlWBjeOBq27JHmzdZ3NS/J7Z4nSs2JyXYRkkCAwEAAQJALV2eykcoC48TonQEPmkg\nbhaIS57syw67jMLsQImQ02UABKzqHPEKLXPOZhZPS9hsC/hGIehwiYCXMUlrl+WF\nAQIhAOntBI6qaecNjAAVG7UbZclMuHROUONmZUF1KNq6VyV5AiEAxRLkfHWy52CM\njOQrX347edZ30f4QczvugXwsyuU9A1ECIGlGZ8Sk4OBA8n6fAUcyO06qnmCJVlHg\npTUeOvKk5c9RAiBs28+8dCNbrbhVhx/yQr9FwNM0+ttJW/yWJ+pyNQhr0QIgJTT6\nxwCWYOtbioyt7B9l+ENy3AMSO3Uq+xmIKkvItK4=\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
// baz.foobar.com
|
||||
var validCert2 = '-----BEGIN CERTIFICATE-----\nMIIBwjCCAWwCCQDIKtL9RCDCkDANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wHhcN\nMTUxMDI4MTMwNTMzWhcNMTYxMDI3MTMwNTMzWjBoMQswCQYDVQQGEwJERTEPMA0G\nA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05lYnVsb24x\nDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEAw7UWW/VoQePv2l92l3XcntZeyw1nBiHxk1axZwC6auOW\n2/zfA//Tg7fv4q5qKnV1n/71IiMAheeFcpfogY5rTwIDAQABMA0GCSqGSIb3DQEB\nCwUAA0EAtluL6dGNfOdNkzoO/UwzRaIvEm2reuqe+Ik4WR/k+DJ4igrmRCQqXwjW\nJaGYsFWsuk3QLOWQ9YgCKlcIYd+1/A==\n-----END CERTIFICATE-----';
|
||||
var validKey2 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBAMO1Flv1aEHj79pfdpd13J7WXssNZwYh8ZNWsWcAumrjltv83wP/\n04O37+Kuaip1dZ/+9SIjAIXnhXKX6IGOa08CAwEAAQJAUPD3Y2cXDJFaJQXwhWnw\nqhzdLbvITUgCor5rNr+dWhE2MopGPpRHiabA1PeWEPx8CfblyTZGd8KUR/2W1c0r\naQIhAP4ZxB3+uhuzzMfyRrn/khr12pFn/FCIDbwnDbyUxLrTAiEAxSuVOFs+Mupt\nYCz/pPrDCx3eid0wyXRObbkLHOxJiBUCIBTp5fxaBNNW3xnt1OhmIo5Zgd3J4zh1\nmjvMMxM8Y1zFAiAxOP0qsZSoj1+41+MGY9fXaaCJ2F96m3+M4tpEYTTGNQIgdESZ\nz+hzHBeYVbWJpIR8uaNkx7wveUF90FpipXyeTsA=\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
it('allows both null', function () {
|
||||
expect(settings.validateCertificate(null, null, 'foobar.com')).to.be(null);
|
||||
});
|
||||
|
||||
it('does not allow only cert', function () {
|
||||
expect(settings.validateCertificate('cert', null, 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow only key', function () {
|
||||
expect(settings.validateCertificate(null, 'key', 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow empty string for cert', function () {
|
||||
expect(settings.validateCertificate('', 'key', 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow empty string for key', function () {
|
||||
expect(settings.validateCertificate('cert', '', 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow invalid cert', function () {
|
||||
expect(settings.validateCertificate('someinvalidcert', validKey0, 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow invalid key', function () {
|
||||
expect(settings.validateCertificate(validCert0, 'invalidkey', 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow cert without matching domain', function () {
|
||||
expect(settings.validateCertificate(validCert0, validKey0, 'cloudron.io')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain', function () {
|
||||
expect(settings.validateCertificate(validCert0, validKey0, 'foobar.com')).to.be(null);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain (wildcard)', function () {
|
||||
expect(settings.validateCertificate(validCert1, validKey1, 'abc.foobar.com')).to.be(null);
|
||||
});
|
||||
|
||||
it('does now allow cert without matching domain (wildcard)', function () {
|
||||
expect(settings.validateCertificate(validCert1, validKey1, 'foobar.com')).to.be.an(Error);
|
||||
expect(settings.validateCertificate(validCert1, validKey1, 'bar.abc.foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain (subdomain)', function () {
|
||||
expect(settings.validateCertificate(validCert2, validKey2, 'baz.foobar.com')).to.be(null);
|
||||
});
|
||||
|
||||
it('does not allow cert without matching domain (subdomain)', function () {
|
||||
expect(settings.validateCertificate(validCert0, validKey0, 'baz.foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow invalid cert/key tuple', function () {
|
||||
expect(settings.validateCertificate(validCert0, validKey1, 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = waitForDns;
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
attempt = require('attempt'),
|
||||
debug = require('debug')('box:src/waitfordns'),
|
||||
dns = require('native-dns');
|
||||
|
||||
// the first arg to callback is not an error argument; this is required for async.every
|
||||
function isChangeSynced(domain, ip, nameserver, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof nameserver, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// ns records cannot have cname
|
||||
dns.resolve4(nameserver, function (error, nsIps) {
|
||||
if (error || !nsIps || nsIps.length === 0) return callback(false);
|
||||
|
||||
async.every(nsIps, function (nsIp, iteratorCallback) {
|
||||
var req = dns.Request({
|
||||
question: dns.Question({ name: domain, type: 'A' }),
|
||||
server: { address: nsIp },
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
req.on('timeout', function () { return iteratorCallback(false); });
|
||||
|
||||
req.on('message', function (error, message) {
|
||||
if (error || !message.answer || message.answer.length === 0) return iteratorCallback(false);
|
||||
|
||||
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, message.answer[0], ip);
|
||||
|
||||
if (message.answer[0].address !== ip) return iteratorCallback(false);
|
||||
|
||||
iteratorCallback(true); // done
|
||||
});
|
||||
|
||||
req.send();
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
// check if IP change has propagated to every nameserver
|
||||
function waitForDns(domain, ip, zoneName, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
|
||||
var defaultOptions = {
|
||||
retryInterval: 5000,
|
||||
retries: Infinity
|
||||
};
|
||||
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = defaultOptions;
|
||||
} else {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
}
|
||||
|
||||
debug('waitForDNS: domain %s to be %s in zone %s.', domain, ip, zoneName);
|
||||
|
||||
attempt(function (attempts) {
|
||||
var callback = this; // gross
|
||||
debug('waitForDNS: %s attempt %s.', domain, attempts);
|
||||
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(error || new Error('Unable to get nameservers'));
|
||||
|
||||
async.every(nameservers, isChangeSynced.bind(null, domain, ip), function (synced) {
|
||||
debug('waitForDNS: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
|
||||
|
||||
callback(synced ? null : new Error('ETRYAGAIN'));
|
||||
});
|
||||
});
|
||||
}, { interval: options.retryInterval, retries: options.retries }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('waitForDNS: %s done.', domain);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user