Compare commits

..

3 Commits

Author SHA1 Message Date
Girish Ramakrishnan 51aaa8f304 More changes 2018-08-16 20:14:03 -07:00
Girish Ramakrishnan 0c2e200176 3.0.2 changes 2018-08-16 20:11:58 -07:00
Girish Ramakrishnan 8d7ba5cc26 Fix issue where normal users are shown all apps 2018-08-16 20:10:57 -07:00
130 changed files with 2502 additions and 5530 deletions
-28
View File
@@ -1,28 +0,0 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2017
},
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
}
+1
View File
@@ -6,3 +6,4 @@ installer/src/certs/server.key
# vim swap files
*.swp
+9
View File
@@ -0,0 +1,9 @@
{
"node": true,
"browser": true,
"unused": true,
"multistr": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
}
+1 -112
View File
@@ -1351,116 +1351,5 @@
[3.0.2]
* Fix issue where normal users are shown apps they don't have access to
* Re-configure email apps when email is enabled/disabled
[3.1.0]
* Add UDP support
* Clicking invite button does not send an invite immediately
* Implement docker addon
* Automatically login after password reset and account setup
* Make backup interval configurable
* Fix alternate domain certificate renewal
[3.1.1]
* Fix caas domain migration
[3.1.2]
* Add UDP support
* Clicking invite button does not send an invite immediately
* Implement docker addon
* Automatically login after password reset and account setup
* Make backup interval configurable
* Fix alternate domain certificate renewal
* API token can now have a name
[3.1.3]
* Prevent dashboard domain from being deleted
* Add alternateDomains to app install route
* cloudflare: Fix crash when access denied
[3.1.4]
* Fix issue where support tab was redirecting
[3.2.0]
* Add DO Spaces SFO2 region
* Wildcard DNS now validates the config
* Add ACMEv2 support
* Add Wildcard Let's Encrypt provider
[3.2.1]
* Add acme2 support. This provides DNS based validation removing inbound port 80 requirement
* Add support for wildcard certificates
* Allow mailbox name to be reset to the buit-in '.app' name
* Fix permission issue when restoring a Cloudron
* Fix a crash when restoring Cloudron
* Allow alternate domains to be set in app installation REST API
* Add SFO2 region for DigitalOcean Spaces
* Show the title in port bindings instead of the long description
[3.2.2]
* Update Haraka to 2.8.20
* (mail) Fix issue where LDAP connections where not cleaned up
[3.3.0]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
[3.3.1]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
[3.3.2]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
* Add domain, mail events to eventlog
[3.3.3]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
* Add domain, mail events to eventlog
[3.3.4]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
* Add domain, mail events to eventlog
* Re-configure mail apps when mail is enabled/disabled
-1
View File
@@ -1 +0,0 @@
# release version. do not edit manually
+4 -21
View File
@@ -14,11 +14,8 @@ function die {
export DEBIAN_FRONTEND=noninteractive
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
apt-mark hold grub* >/dev/null
apt-get -o Dpkg::Options::="--force-confdef" update -y
apt-get -o Dpkg::Options::="--force-confdef" upgrade -y
apt-mark unhold grub* >/dev/null
apt-get -o Dpkg::Options::="--force-confdef" dist-upgrade -y
echo "==> Installing required packages"
@@ -26,7 +23,6 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password password pas
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
apt-get -y install \
acl \
awscli \
@@ -35,14 +31,12 @@ apt-get -y install \
curl \
dmsetup \
iptables \
libpython2.7 \
logrotate \
mysql-server-5.7 \
nginx-full \
openssh-server \
pwgen \
resolvconf \
sudo \
rcconf \
swaks \
unattended-upgrades \
unbound \
@@ -78,9 +72,8 @@ if [[ "${storage_driver}" != "overlay2" ]]; then
exit 1
fi
# do not upgrade grub because it might prompt user and break this script
echo "==> Enable memory accounting"
apt-get -y --no-upgrade install grub2-common
apt-get -y install grub2
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
update-grub
@@ -90,12 +83,11 @@ if [ ! -f "${arg_infraversionpath}/infra_version.js" ]; then
exit 1
fi
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo -e "\tPulling docker images: ${images}"
for image in ${images}; do
docker pull "${image}"
docker pull "${image%@sha256:*}" # this will tag the image for readability
done
echo "==> Install collectd"
@@ -105,11 +97,6 @@ if ! apt-get install -y collectd collectd-utils; then
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
fi
echo "==> Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
systemctl stop bind9 || true
systemctl disable bind9 || true
@@ -122,7 +109,3 @@ systemctl disable dnsmasq || true
systemctl stop postfix || true
systemctl disable postfix || true
# on ubuntu 18.04, this is the default. this requires resolvconf for DNS to work further after the disable
systemctl stop systemd-resolved || true
systemctl disable systemd-resolved || true
+3 -12
View File
@@ -9,10 +9,10 @@ require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
let async = require('async'),
var appHealthMonitor = require('./src/apphealthmonitor.js'),
async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
dockerProxy = require('./src/dockerproxy.js'),
server = require('./src/server.js');
console.log();
@@ -25,9 +25,6 @@ console.log(' Version: ', config.version());
console.log(' Admin Origin: ', config.adminOrigin());
console.log(' Appstore API server origin: ', config.apiServerOrigin());
console.log(' Appstore Web server origin: ', config.webServerOrigin());
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
console.log(' LDAP Server Port: ', config.get('ldapPort'));
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
console.log();
console.log('==========================================');
console.log();
@@ -35,7 +32,7 @@ console.log();
async.series([
server.start,
ldap.start,
dockerProxy.start
appHealthMonitor.start,
], function (error) {
if (error) {
console.error('Error starting server', error);
@@ -47,19 +44,13 @@ async.series([
var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () {
console.log('Received SIGINT. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
console.log('Received SIGTERM. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
@@ -4,7 +4,7 @@ var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * from apps', [ ], function (error, results) {
if (error) return callback(error);
if (error) return done(error);
var queries = [
db.runSql.bind(db, 'START TRANSACTION;')
@@ -1,18 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE appPortBindings ADD COLUMN type VARCHAR(8) NOT NULL DEFAULT "tcp"'),
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP INDEX hostPort'), // this drops the unique constraint
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort, type)')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort)'),
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP COLUMN type')
], callback);
};
@@ -1,16 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
backupConfig.intervalSecs = 24 * 60 * 60;
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,23 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
// first check precondtion of domain entry in settings
db.all('SELECT * FROM domains', [ ], function (error, domains) {
if (error) return callback(error);
let caasDomains = domains.filter(function (d) { return d.provider === 'caas'; });
async.eachSeries(caasDomains, function (domain, iteratorCallback) {
let config = JSON.parse(domain.configJson);
config.hyphenatedSubdomains = true;
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,12 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tokens ADD COLUMN name VARCHAR(64) DEFAULT ""', [], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tokens DROP COLUMN name', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,21 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * from domains WHERE provider=?', [ 'manual' ], function (error, results) {
if (error) return callback(error);
async.eachSeries(results, function (result, iteratorDone) {
var config = JSON.parse(result.configJson || '{}');
if (!config.wildcard) return iteratorDone();
delete config.wildcard;
db.runSql('UPDATE domains SET provider=?, configJson=? WHERE domain=?', [ 'wildcard', JSON.stringify(config), result.domain ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,21 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS appEnvVars(' +
'appId VARCHAR(128) NOT NULL,' +
'name TEXT NOT NULL,' +
'value TEXT NOT NULL,' +
'FOREIGN KEY(appId) REFERENCES apps(id)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE appEnvVars', function (error) {
if (error) console.error(error);
callback(error);
});
};
+1 -9
View File
@@ -41,7 +41,6 @@ CREATE TABLE IF NOT EXISTS groupMembers(
FOREIGN KEY(userId) REFERENCES users(id));
CREATE TABLE IF NOT EXISTS tokens(
name VARCHAR(64) DEFAULT "", // description
accessToken VARCHAR(128) NOT NULL UNIQUE,
identifier VARCHAR(128) NOT NULL,
clientId VARCHAR(128),
@@ -51,7 +50,7 @@ CREATE TABLE IF NOT EXISTS tokens(
CREATE TABLE IF NOT EXISTS clients(
id VARCHAR(128) NOT NULL UNIQUE, // prefixed with cid- to identify token easily in auth routes
appId VARCHAR(128) NOT NULL, // name of the client (for external apps) or id of app (for built-in apps)
appId VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL,
clientSecret VARCHAR(512) NOT NULL,
redirectURI VARCHAR(512) NOT NULL,
@@ -93,7 +92,6 @@ CREATE TABLE IF NOT EXISTS apps(
CREATE TABLE IF NOT EXISTS appPortBindings(
hostPort INTEGER NOT NULL UNIQUE,
type VARCHAR(8) NOT NULL DEFAULT "tcp",
environmentVariable VARCHAR(128) NOT NULL,
appId VARCHAR(128) NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id),
@@ -118,12 +116,6 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
value VARCHAR(512) NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS appEnvVars(
appId VARCHAR(128) NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
creationTime TIMESTAMP,
+29 -149
View File
@@ -1632,15 +1632,15 @@
}
},
"cloudron-manifestformat": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.14.2.tgz",
"integrity": "sha512-+VQwlP/2NY0VIjPTkANhlg8DrS62IxkAVS7B7KG6DrzRp+hRCejbDMQjUB8GvyPSVQGerqUoEu5R/tc4/UBiAA==",
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.11.0.tgz",
"integrity": "sha512-t4KR2KmK1JDtxw1n6IVNg0+xxspk/Cpb6m1WimE9hJ0KJYsIgZNnkce47uYiG9/nWrgUSV4xcdzsS91OOvWgig==",
"requires": {
"cron": "1.3.0",
"java-packagename-regex": "1.0.0",
"java-packagename-regex": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"safetydance": "0.0.15",
"semver": "4.3.6",
"tv4": "1.3.0",
"tv4": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
"validator": "3.43.0"
},
"dependencies": {
@@ -1708,41 +1708,6 @@
"typedarray": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
}
},
"connect": {
"version": "3.6.6",
"resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
"integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
"requires": {
"debug": "2.6.9",
"finalhandler": "1.1.0",
"parseurl": "1.3.2",
"utils-merge": "1.0.1"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"finalhandler": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
"integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
"requires": {
"debug": "2.6.9",
"encodeurl": "1.0.2",
"escape-html": "1.0.3",
"on-finished": "2.3.0",
"parseurl": "1.3.2",
"statuses": "1.3.1",
"unpipe": "1.0.0"
}
}
}
},
"connect-ensure-login": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz",
@@ -2717,11 +2682,6 @@
"safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"ejs": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz",
@@ -3232,18 +3192,22 @@
"dependencies": {
"mkdirp": {
"version": "0.3.5",
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=",
"dev": true
},
"rimraf": {
"version": "2.2.8",
"resolved": "http://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=",
"dev": true
}
}
},
"fs.realpath": {
"version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fstream": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
@@ -3984,6 +3948,18 @@
}
}
},
"glob": {
"version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"requires": {
"fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"once": "1.4.0",
"path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
}
},
"globule": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
@@ -4514,8 +4490,7 @@
}
},
"java-packagename-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"version": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"integrity": "sha1-lR9he9WhlCIO0GcLm4KowOxcYiQ="
},
"js-base64": {
@@ -4576,7 +4551,7 @@
},
"jsonfile": {
"version": "1.0.1",
"resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
"integrity": "sha1-6l7+QLg2kLmGZ2FKc5L8YOhCwN0=",
"dev": true
},
@@ -5280,11 +5255,6 @@
}
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"multiparty": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.1.4.tgz",
@@ -5433,7 +5403,7 @@
},
"ncp": {
"version": "0.4.2",
"resolved": "http://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz",
"integrity": "sha1-q8xsvT7C7Spyn/bnwfqPAXhKhXQ=",
"dev": true
},
@@ -5994,14 +5964,6 @@
}
}
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -6151,11 +6113,6 @@
"resolved": "https://registry.npmjs.org/parse-links/-/parse-links-0.1.0.tgz",
"integrity": "sha1-afpighugBBX+c2MyNVIeRUe36CE="
},
"parseurl": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
"integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
},
"passport": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz",
@@ -7353,75 +7310,9 @@
"rimraf": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
"integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=",
"requires": {
"glob": "7.1.3"
},
"dependencies": {
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "1.1.11"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
"glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz"
}
},
"s3-block-read-stream": {
@@ -7982,11 +7873,6 @@
}
}
},
"statuses": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
"integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
},
"stdout-stream": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz",
@@ -8593,8 +8479,7 @@
"dev": true
},
"tv4": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
"version": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
"integrity": "sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM="
},
"tweetnacl": {
@@ -8651,11 +8536,6 @@
"version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
+3 -3
View File
@@ -20,8 +20,7 @@
"async": "^2.6.1",
"aws-sdk": "^2.253.1",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.14.2",
"connect": "^3.6.6",
"cloudron-manifestformat": "^2.11.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
"connect-timeout": "^1.9.0",
@@ -92,7 +91,8 @@
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test/[^a]*",
"test_all": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
"postmerge": "/bin/true",
"precommit": "/bin/true",
"prepush": "npm test",
+122
View File
@@ -0,0 +1,122 @@
#!/bin/bash
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
function get_status() {
key="$1"
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
echo "${currentValue}"
return 0
fi
return 1
}
function wait_for_status() {
key="$1"
expectedValue="$2"
echo "wait_for_status: $key to be $expectedValue"
while true; do
if currentValue=$(get_status "${key}"); then
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
if [[ "${currentValue}" == $expectedValue ]]; then
break
fi
fi
sleep 3
done
}
domain=""
domainProvider=""
domainConfigJson="{}"
domainTlsProvider="letsencrypt-prod"
adminUsername="superadmin"
adminPassword="Secret123#"
adminEmail="admin@server.local"
appstoreUserId=""
appstoreToken=""
backupDir="/var/backups"
args=$(getopt -o "" -l "domain:,domain-provider:,domain-tls-provider:,admin-username:,admin-password:,admin-email:,appstore-user:,appstore-token:,backup-dir:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--domain) domain="$2"; shift 2;;
--domain-provider) domainProvider="$2"; shift 2;;
--domain-tls-provider) domainTlsProvider="$2"; shift 2;;
--admin-username) adminUsername="$2"; shift 2;;
--admin-password) adminPassword="$2"; shift 2;;
--admin-email) adminEmail="$2"; shift 2;;
--appstore-user) appstoreUser="$2"; shift 2;;
--appstore-token) appstoreToken="$2"; shift 2;;
--backup-dir) backupDir="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "=> Waiting for cloudron to be ready"
wait_for_status "version" '*'
if [[ $(get_status "webadminStatus") != *'"tls": true'* ]]; then
echo "=> Domain setup"
dnsSetupData=$(printf '{ "domain": "%s", "adminFqdn": "%s", "provider": "%s", "config": %s, "tlsConfig": { "provider": "%s" } }' "${domain}" "my.${domain}" "${domainProvider}" "$domainConfigJson" "${domainTlsProvider}")
if ! $curl -X POST -H "Content-Type: application/json" -d "${dnsSetupData}" http://localhost:3000/api/v1/cloudron/dns_setup; then
echo "DNS Setup Failed"
exit 1
fi
wait_for_status "webadminStatus" '*"tls": true*'
else
echo "=> Skipping Domain setup"
fi
activationData=$(printf '{"username": "%s", "password":"%s", "email": "%s" }' "${adminUsername}" "${adminPassword}" "${adminEmail}")
if [[ $(get_status "activated") == "false" ]]; then
echo "=> Activating"
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/cloudron/activate); then
echo "Failed to activate with ${activationData}: ${activationResult}"
exit 1
fi
wait_for_status "activated" "true"
else
echo "=> Skipping Activation"
fi
echo "=> Getting token"
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/developer/login); then
echo "Failed to login with ${activationData}: ${activationResult}"
exit 1
fi
accessToken=$(echo "${activationResult}" | python3 -c 'import sys, json; print(json.load(sys.stdin)[sys.argv[1]])' "accessToken")
echo "=> Setting up App Store account with accessToken ${accessToken}"
appstoreData=$(printf '{"userId":"%s", "token":"%s" }' "${appstoreUser}" "${appstoreToken}")
if ! appstoreResult=$($curl -X POST -H "Content-Type: application/json" -d "${appstoreData}" "http://localhost:3000/api/v1/settings/appstore_config?access_token=${accessToken}"); then
echo "Failed to setup Appstore account with ${appstoreData}: ${appstoreResult}"
exit 1
fi
echo "=> Setting up Backup Directory with accessToken ${accessToken}"
backupData=$(printf '{"provider":"filesystem", "key":"", "backupFolder":"%s", "retentionSecs": 864000, "format": "tgz", "externalDisk": true}' "${backupDir}")
chown -R yellowtent:yellowtent "${backupDir}"
if ! backupResult=$($curl -X POST -H "Content-Type: application/json" -d "${backupData}" "http://localhost:3000/api/v1/settings/backup_config?access_token=${accessToken}"); then
echo "Failed to setup backup configuration with ${backupDir}: ${backupResult}"
exit 1
fi
echo "=> Done!"
-106
View File
@@ -1,106 +0,0 @@
#!/bin/bash
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
ip=""
dns_config=""
tls_cert_file=""
tls_key_file=""
license_file=""
backup_config=""
args=$(getopt -o "" -l "ip:,backup-config:,license:,dns-config:,tls-cert:,tls-key:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--ip) ip="$2"; shift 2;;
--dns-config) dns_config="$2"; shift 2;;
--tls-cert) tls_cert_file="$2"; shift 2;;
--tls-key) tls_key_file="$2"; shift 2;;
--license) license_file="$2"; shift 2;;
--backup-config) backup_config="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# validate arguments in the absence of data
if [[ -z "${ip}" ]]; then
echo "--ip is required"
exit 1
fi
if [[ -z "${dns_config}" ]]; then
echo "--dns-config is required"
exit 1
fi
if [[ ! -f "${license_file}" ]]; then
echo "--license must be a valid license file"
exit 1
fi
function get_status() {
key="$1"
if status=$($curl -q -f -k "https://${ip}/api/v1/cloudron/status" 2>/dev/null); then
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
echo "${currentValue}"
return 0
fi
return 1
}
function wait_for_status() {
key="$1"
expectedValue="$2"
echo "wait_for_status: $key to be $expectedValue"
while true; do
if currentValue=$(get_status "${key}"); then
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
if [[ "${currentValue}" == $expectedValue ]]; then
break
fi
fi
sleep 3
done
}
echo "=> Waiting for cloudron to be ready"
wait_for_status "version" '*'
domain=$(echo "${dns_config}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["domain"])')
echo "Provisioning Cloudron ${domain}"
if [[ -n "${tls_cert_file}" && -n "${tls_key_file}" ]]; then
tls_cert=$(cat "${tls_cert_file}" | awk '{printf "%s\\n", $0}')
tls_key=$(cat "${tls_key_file}" | awk '{printf "%s\\n", $0}')
fallback_cert=$(printf '{ "cert": "%s", "key": "%s", "provider": "fallback", "restricted": true }' "${tls_cert}" "${tls_key}")
else
fallback_cert=None
fi
tls_config='{ "provider": "fallback" }'
dns_config=$(echo "${dns_config}" | python3 -c "import json,sys;obj=json.load(sys.stdin);obj.update(tlsConfig=${tls_config});obj.update(fallbackCertficate=${fallback_cert});print(json.dumps(obj))")
license=$(cat "${license_file}")
if [[ -z "${backup_config:-}" ]]; then
backup_config='{ "provider": "filesystem", "backupFolder": "/var/backups", "format": "tgz" }'
fi
setupData=$(printf '{ "dnsConfig": %s, "autoconf": { "appstoreConfig": %s, "backupConfig": %s } }' "${dns_config}" "${license}" "${backup_config}")
if ! setupResult=$($curl -kq -X POST -H "Content-Type: application/json" -d "${setupData}" https://${ip}/api/v1/cloudron/setup); then
echo "Failed to setup with ${setupData} ${setupResult}"
exit 1
fi
wait_for_status "webadminStatus" '*"tls": true*'
echo "Cloudron is ready at https://my-${domain}"
+31 -52
View File
@@ -4,6 +4,7 @@ set -eu -o pipefail
# change this to a hash when we make a upgrade release
readonly LOG_FILE="/var/log/cloudron-setup.log"
readonly DATA_FILE="/root/cloudron-install-data.json"
readonly MINIMUM_DISK_SIZE_GB="18" # this is the size of "/" and required to fit in docker images 18 is a safe bet for different reporting on 20GB min
readonly MINIMUM_MEMORY="974" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989, Serverdiscounter.com 974)
@@ -43,34 +44,37 @@ fi
initBaseImage="true"
# provisioning data
provider=""
edition=""
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
prerelease="false"
sourceTarballUrl=""
rebootServer="true"
baseDataDir=""
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,edition:,skip-reboot" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,skip-reboot" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
--provider) provider="$2"; shift 2;;
--edition) edition="$2"; shift 2;;
--version) requestedVersion="$2"; shift 2;;
--env)
if [[ "$2" == "dev" ]]; then
apiServerOrigin="https://api.dev.cloudron.io"
webServerOrigin="https://dev.cloudron.io"
prerelease="true"
elif [[ "$2" == "staging" ]]; then
apiServerOrigin="https://api.staging.cloudron.io"
webServerOrigin="https://staging.cloudron.io"
prerelease="true"
fi
shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--prerelease) prerelease="true"; shift;;
--data-dir) baseDataDir=$(realpath "$2"); shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
@@ -83,18 +87,14 @@ if [[ ${EUID} -ne 0 ]]; then
fi
# Only --help works with mismatched ubuntu
ubuntu_version=$(lsb_release -rs)
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" ]]; then
echo "Cloudron requires Ubuntu 16.04 or 18.04" > /dev/stderr
if [[ $(lsb_release -rs) != "16.04" ]]; then
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
exit 1
fi
# Can only write after we have confirmed script has root access
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
# validate arguments in the absence of data
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
@@ -104,8 +104,6 @@ elif [[ \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "galaxygate" && \
"${provider}" != "digitalocean" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "lightsail" && \
@@ -116,7 +114,7 @@ elif [[ \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
@@ -139,18 +137,18 @@ echo ""
if [[ "${initBaseImage}" == "true" ]]; then
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories. See ${LOG_FILE}"
echo "Could not update package repositories"
exit 1
fi
if ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
echo "Could not install setup dependencies (curl)"
exit 1
fi
fi
echo "=> Checking version"
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?boxVersion=${requestedVersion}"); then
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
@@ -166,6 +164,18 @@ if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=
exit 1
fi
# Build data
# from 1.9, we use autoprovision.json
data=$(cat <<EOF
{
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
}
EOF
)
echo "=> Downloading version ${version} ..."
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
@@ -183,44 +193,13 @@ if [[ "${initBaseImage}" == "true" ]]; then
echo ""
fi
# NOTE: this install script only supports 3.x and above
echo "=> Installing version ${version} (this takes some time) ..."
if [[ "${version}" =~ 3\.[0-2]+\.[0-9]+ ]]; then
readonly DATA_FILE="/root/cloudron-install-data.json"
data=$(cat <<EOF
{
"provider": "${provider}",
"edition": "${edition}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
}
EOF
)
echo "${data}" > "${DATA_FILE}"
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
rm "${DATA_FILE}"
else
mkdir -p /etc/cloudron
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
{
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"provider": "${provider}",
"edition": "${edition}"
}
CONF_END
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
echo "${data}" > "${DATA_FILE}"
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
rm "${DATA_FILE}"
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
-89
View File
@@ -1,89 +0,0 @@
#!/bin/bash
# This script collects diagnostic information to help debug server related issues
# It also enables SSH access for the cloudron support team
PASTEBIN="https://paste.cloudron.io"
OUT="/tmp/cloudron-support.log"
LINE="\n========================================================\n"
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
# We require root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
# check if at least 10mb root partition space is available
if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
echo "No more space left on /"
echo "This is likely the root case of the issue. Free up some space and also check other partitions below:"
echo ""
df -h
echo ""
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/server/#recovery-after-disk-full"
exit 1
fi
# check for at least 5mb free /tmp space for the log file
if [[ "`df --output="avail" /tmp | sed -n 2p`" -lt "5120" ]]; then
echo "Not enough space left on /tmp"
echo "Free up some space first by deleting files from /tmp"
exit 1
fi
echo -n "Generating Cloudron Support stats..."
# clear file
rm -rf $OUT
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
if [[ $SUDO_USER == "" ]]; then
ssh_user="root"
ssh_folder="/root/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
else
ssh_user="$SUDO_USER"
ssh_folder="/home/$SUDO_USER/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
fi
echo -e $LINE"SSH"$LINE >> $OUT
echo "Username: ${ssh_user}" >> $OUT
echo "Port: ${ssh_port}" >> $OUT
echo -e $LINE"cloudron.conf"$LINE >> $OUT
cat /etc/cloudron/cloudron.conf &>> $OUT
echo -e $LINE"Docker container"$LINE >> $OUT
if ! timeout --kill-after 10s 15s docker ps -a &>> $OUT 2>&1; then
echo -e "Docker is not responding" >> $OUT
fi
echo -e $LINE"Filesystem stats"$LINE >> $OUT
df -h &>> $OUT
echo -e $LINE"System daemon status"$LINE >> $OUT
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
echo -e $LINE"Firewall chains"$LINE >> $OUT
iptables -L &>> $OUT
echo "Done"
echo -n "Uploading information..."
# for some reason not using $(cat $OUT) will not contain newlines!?
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
echo "Done"
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p "${ssh_folder}"
echo "${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
chown -R ${ssh_user} "${ssh_folder}"
chmod 600 "${authorized_key_file}"
echo "Done"
echo ""
echo "Please email the following link to support@cloudron.io"
echo ""
echo "${PASTEBIN}/${paste_key}"
+7 -11
View File
@@ -7,28 +7,21 @@ set -eu
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "output:,version:" -n "$0" -- "$@")
args=$(${GNU_GETOPT} -o "" -l "output:" -n "$0" -- "$@")
eval set -- "${args}"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
bundle_file=""
version=""
while true; do
case "$1" in
--output) bundle_file="$2"; shift 2;;
--version) version="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
if [[ -z "${version}" ]]; then
echo "--version is required"
exit 1
fi
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
@@ -48,16 +41,19 @@ fi
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
branch=$(git rev-parse --abbrev-ref HEAD)
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "${branch}")
if [[ "${branch}" == "master" ]]; then
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git rev-parse "${branch}")
else
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "origin/${branch}")
fi
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}-${version}.tar.gz"
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}.tar.gz"
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
echo "==> Checking out code box version [${box_version}] and dashboard version [${dashboard_version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
(cd "${SOURCE_DIR}/../dashboard" && git archive --format=tar ${dashboard_version} | (mkdir -p "${bundle_dir}/dashboard.build" && cd "${bundle_dir}/dashboard.build" && tar xf -))
(cp "${SOURCE_DIR}/../dashboard/LICENSE" "${bundle_dir}")
echo "${version}" > "${bundle_dir}/VERSION"
echo "==> Installing modules for dashboard asset generation"
(cd "${bundle_dir}/dashboard.build" && npm install --production)
+28 -15
View File
@@ -1,9 +1,5 @@
#!/bin/bash
# This script is run before the box code is switched. This means that we can
# put network related/curl downloads here. If the script fails, the old code
# will continue to run
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
@@ -14,12 +10,29 @@ fi
readonly USER=yellowtent
readonly BOX_SRC_DIR=/home/${USER}/box
readonly BASE_DATA_DIR=/home/${USER}
readonly CLOUDRON_CONF=/home/yellowtent/configs/cloudron.conf
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly box_src_tmp_dir="$(realpath ${script_dir}/..)"
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
readonly is_update=$([[ -f "${CLOUDRON_CONF}" ]] && echo "yes" || echo "no")
arg_data=""
arg_data_dir=""
args=$(getopt -o "" -l "data:,data-file:,data-dir:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--data) arg_data="$2"; shift 2;;
--data-file) arg_data=$(cat $2); shift 2;;
--data-dir) arg_data_dir="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
@@ -81,15 +94,6 @@ if [[ ${try} -eq 10 ]]; then
exit 4
fi
echo "==> installer: downloading new addon images"
images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo -e "\tPulling docker images: ${images}"
for image in ${images}; do
docker pull "${image}" # this pulls the image using the sha256
docker pull "${image%@sha256:*}" # this will tag the image for readability
done
echo "==> installer: update cloudron-syslog"
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
CLOUDRON_SYSLOG="${CLOUDRON_SYSLOG_DIR}/bin/cloudron-syslog"
@@ -111,6 +115,15 @@ if [[ "${is_update}" == "yes" ]]; then
${BOX_SRC_DIR}/setup/stop.sh
fi
# setup links to data directory
if [[ -n "${arg_data_dir}" ]]; then
echo "==> installer: setting up links to data directory"
mkdir "${arg_data_dir}/appsdata"
ln -s "${arg_data_dir}/appsdata" "${BASE_DATA_DIR}/appsdata"
mkdir "${arg_data_dir}/platformdata"
ln -s "${arg_data_dir}/platformdata" "${BASE_DATA_DIR}/platformdata"
fi
# ensure we are not inside the source directory, which we will remove now
cd /root
@@ -120,4 +133,4 @@ mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}"
echo "==> installer: calling box setup script"
"${BOX_SRC_DIR}/setup/start.sh"
"${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}"
+71
View File
@@ -0,0 +1,71 @@
#!/bin/bash
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
json="${source_dir}/../node_modules/.bin/json"
arg_api_server_origin=""
arg_fqdn="" # remove after 1.10
arg_admin_domain=""
arg_admin_location=""
arg_admin_fqdn=""
arg_retire_reason=""
arg_retire_info=""
arg_version=""
arg_web_server_origin=""
arg_provider=""
arg_is_demo="false"
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--retire-reason)
arg_retire_reason="$2"
shift 2
;;
--retire-info)
arg_retire_info="$2"
shift 2
;;
--data)
# these params must be valid in all cases
arg_fqdn=$(echo "$2" | $json fqdn)
arg_admin_fqdn=$(echo "$2" | $json adminFqdn)
arg_admin_location=$(echo "$2" | $json adminLocation)
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
arg_admin_domain=$(echo "$2" | $json adminDomain)
[[ "${arg_admin_domain}" == "" ]] && arg_admin_domain="${arg_fqdn}"
# only update/restore have this valid (but not migrate)
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
[[ "${arg_web_server_origin}" == "" ]] && arg_web_server_origin="https://cloudron.io"
# TODO check if and where this is used
arg_version=$(echo "$2" | $json version)
# read possibly empty parameters here
arg_is_demo=$(echo "$2" | $json isDemo)
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
arg_provider=$(echo "$2" | $json provider)
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
shift 2
;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "Parsed arguments:"
echo "api server: ${arg_api_server_origin}"
echo "admin fqdn: ${arg_admin_fqdn}"
echo "fqdn: ${arg_fqdn}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
echo "provider: ${arg_provider}"
+50 -15
View File
@@ -2,9 +2,6 @@
set -eu -o pipefail
# This script is run after the box code is switched. This means that this script
# should pretty much always succeed. No network logic/download code here.
echo "==> Cloudron Start"
readonly USER="yellowtent"
@@ -15,8 +12,17 @@ readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
readonly CONFIG_DIR="${HOME_DIR}/configs"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly get_config="$(realpath ${script_dir}/../node_modules/.bin/json) -f /etc/cloudron/cloudron.conf"
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
echo "==> Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
hostnamectl set-hostname "${arg_admin_fqdn}"
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
@@ -42,6 +48,19 @@ if [[ ! -f /etc/systemd/system/docker.service.d/cloudron.conf ]] || ! diff -q /e
fi
docker network create --subnet=172.18.0.0/16 cloudron || true
# caas has ssh on port 202 and we disable password login
if [[ "${arg_provider}" == "caas" ]]; then
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
sed -e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
-e 's/^#\?Port .*/Port 202/g' \
-i /etc/ssh/sshd_config
# required so we can connect to this machine since port 22 is blocked by iptables by now
systemctl reload sshd
fi
mkdir -p "${BOX_DATA_DIR}"
mkdir -p "${APPS_DATA_DIR}"
@@ -52,7 +71,6 @@ mkdir -p "${PLATFORM_DATA_DIR}/graphite"
mkdir -p "${PLATFORM_DATA_DIR}/mysql"
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
mkdir -p "${PLATFORM_DATA_DIR}/redis"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
@@ -91,14 +109,6 @@ setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
echo "==> Creating config directory"
mkdir -p "${CONFIG_DIR}"
# migration for cloudron.conf file. Can be removed after 3.3
if [[ ! -d /etc/cloudron ]]; then
echo "==> Migrating existing cloudron.conf to new location"
mkdir -p /etc/cloudron
cp "${CONFIG_DIR}/cloudron.conf" /etc/cloudron/cloudron.conf
fi
chown -R "${USER}" /etc/cloudron
echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
@@ -161,8 +171,14 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
systemctl daemon-reload
fi
# remove this migration after 1.10
[[ -f /etc/nginx/cert/host.cert ]] && cp /etc/nginx/cert/host.cert "/etc/nginx/cert/${arg_admin_domain}.host.cert"
[[ -f /etc/nginx/cert/host.key ]] && cp /etc/nginx/cert/host.key "/etc/nginx/cert/${arg_admin_domain}.host.key"
systemctl start nginx
# bookkeep the version as part of data
echo "{ \"version\": \"${arg_version}\", \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${BOX_DATA_DIR}/version"
# restart mysql to make sure it has latest config
if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf >/dev/null; then
# wait for all running mysql jobs
@@ -192,6 +208,27 @@ cd "${BOX_SRC_DIR}"
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF
echo "==> Creating cloudron.conf"
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{
"version": "${arg_version}",
"apiServerOrigin": "${arg_api_server_origin}",
"webServerOrigin": "${arg_web_server_origin}",
"adminDomain": "${arg_admin_domain}",
"adminFqdn": "${arg_admin_fqdn}",
"adminLocation": "${arg_admin_location}",
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo}
}
CONF_END
echo "==> Creating config.json for dashboard"
cat > "${BOX_SRC_DIR}/dashboard/dist/config.json" <<CONF_END
{
"webServerOrigin": "${arg_web_server_origin}"
}
CONF_END
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
echo "==> Generating dhparams (takes forever)"
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
@@ -202,11 +239,9 @@ fi
echo "==> Changing ownership"
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update"
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
chown "${USER}:${USER}" "${APPS_DATA_DIR}"
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
chown root:root -R "${PLATFORM_DATA_DIR}/logrotate.d"
+1 -1
View File
@@ -90,8 +90,8 @@ server {
add_header Referrer-Policy "no-referrer-when-downgrade";
proxy_hide_header Referrer-Policy;
<% if ( endpoint === 'admin' ) { -%>
# CSP headers for the admin/dashboard resources
<% if ( endpoint === 'admin' ) { -%>
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
<% } -%>
+4 -4
View File
@@ -1,11 +1,11 @@
# sudo logging breaks journalctl output with very long urls (systemd bug)
Defaults !syslog
Defaults!/home/yellowtent/box/src/scripts/rmvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmvolume.sh
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh
Defaults!/home/yellowtent/box/src/scripts/rmaddon.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddon.sh
Defaults!/home/yellowtent/box/src/scripts/rmappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmappdir.sh
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
+6 -2
View File
@@ -26,9 +26,9 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:accesscontrol'),
settings = require('./settings.js'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
UsersError = users.UsersError,
@@ -114,7 +114,11 @@ function scopesForUser(user, callback) {
if (user.admin) return callback(null, exports.VALID_SCOPES);
callback(null, config.isSpacesEnabled() ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
settings.getSpacesConfig(function (error, spaces) {
if (error) return callback(error);
callback(null, spaces.enabled ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
});
}
function validateToken(accessToken, callback) {
+202 -774
View File
File diff suppressed because it is too large Load Diff
+28 -71
View File
@@ -71,9 +71,7 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.ts' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
const SUBDOMAIN_FIELDS = [ 'appId', 'domain', 'subdomain', 'type' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
@@ -98,16 +96,14 @@ function postProcess(result) {
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
result.portBindings = { };
let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(',');
var hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
var environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
delete result.hostPorts;
delete result.environmentVariables;
delete result.portTypes;
for (let i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
for (var i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
}
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
@@ -130,14 +126,6 @@ function postProcess(result) {
delete d.appId;
delete d.type;
});
let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues);
delete result.envNames;
delete result.envValues;
result.env = {};
for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty
if (envNames[i]) result.env[envNames[i]] = envValues[i];
}
}
function get(id, callback) {
@@ -145,17 +133,15 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, '
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
result[0].alternateDomains = alternateDomains;
@@ -163,7 +149,7 @@ function get(id, callback) {
postProcess(result[0]);
callback(null, result[0]);
});
})
});
}
@@ -172,17 +158,15 @@ function getByHttpPort(httpPort, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
result[0].alternateDomains = alternateDomains;
@@ -198,17 +182,15 @@ function getByContainerId(containerId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
result[0].alternateDomains = alternateDomains;
@@ -223,16 +205,14 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
database.query('SELECT * FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
alternateDomains.forEach(function (d) {
@@ -275,7 +255,6 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
var sso = 'sso' in data ? data.sso : null;
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var env = data.env || {};
var queries = [];
@@ -292,15 +271,8 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
Object.keys(portBindings).forEach(function (env) {
queries.push({
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)',
args: [ env, portBindings[env].hostPort, portBindings[env].type, id ]
});
});
Object.keys(env).forEach(function (name) {
queries.push({
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
args: [ id, name, env[name] ]
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, appId) VALUES (?, ?, ?)',
args: [ env, portBindings[env], id ]
});
});
@@ -350,19 +322,18 @@ function getPortBindings(id, callback) {
var portBindings = { };
for (var i = 0; i < results.length; i++) {
portBindings[results[i].environmentVariable] = { hostPort: results[i].hostPort, type: results[i].type };
portBindings[results[i].environmentVariable] = results[i].hostPort;
}
callback(null, portBindings);
});
}
function delPortBinding(hostPort, type, callback) {
function delPortBinding(hostPort, callback) {
assert.strictEqual(typeof hostPort, 'number');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) {
database.query('DELETE FROM appPortBindings WHERE hostPort=?', [ hostPort ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -378,13 +349,12 @@ function del(id, callback) {
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
];
database.transaction(queries, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results[4].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
@@ -397,7 +367,6 @@ function clear(callback) {
database.query.bind(null, 'DELETE FROM subdomains'),
database.query.bind(null, 'DELETE FROM appPortBindings'),
database.query.bind(null, 'DELETE FROM appAddonConfigs'),
database.query.bind(null, 'DELETE FROM appEnvVars'),
database.query.bind(null, 'DELETE FROM apps')
], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -417,7 +386,6 @@ function updateWithConstraints(id, app, constraints, callback) {
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
assert(!('env' in app) || typeof app.env === 'object');
var queries = [ ];
@@ -426,19 +394,8 @@ function updateWithConstraints(id, app, constraints, callback) {
// replace entries by app id
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
Object.keys(portBindings).forEach(function (env) {
var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values });
});
}
if ('env' in app) {
queries.push({ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] });
Object.keys(app.env).forEach(function (name) {
queries.push({
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
args: [ id, name, app.env[name] ]
});
var values = [ portBindings[env], env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, environmentVariable, appId) VALUES(?, ?, ?)', args: values });
});
}
@@ -462,7 +419,7 @@ function updateWithConstraints(id, app, constraints, callback) {
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains') {
fields.push(p + ' = ?');
values.push(app[p]);
}
@@ -655,7 +612,7 @@ function transferOwnership(oldOwnerId, newOwnerId, callback) {
assert.strictEqual(typeof newOwnerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error) {
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error, results) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
+54 -45
View File
@@ -12,14 +12,15 @@ var appdb = require('./appdb.js'),
util = require('util');
exports = module.exports = {
run: run
start: start,
stop: stop
};
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
var gHealthInfo = { }; // { time, emailSent }
const NOOP_CALLBACK = function (error) { if (error) console.error(error); };
var gRunTimeout = null;
var gDockerEventStream = null;
function debugApp(app) {
assert(typeof app === 'object');
@@ -87,9 +88,6 @@ function checkAppHealth(app, callback) {
return setHealth(app, appdb.HEALTH_DEAD, callback);
}
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return setHealth(app, appdb.HEALTH_HEALTHY, callback);
// poll through docker network instead of nginx to bypass any potential oauth proxy
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
superagent
@@ -112,23 +110,48 @@ function checkAppHealth(app, callback) {
});
}
function processApps(callback) {
apps.getAll(function (error, result) {
if (error) return callback(error);
async.each(result, checkAppHealth, function (error) {
if (error) console.error(error);
var alive = result
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
debug('apps alive: [%s]', alive);
callback(null);
});
});
}
function run() {
processApps(function (error) {
if (error) console.error(error);
gRunTimeout = setTimeout(run, HEALTHCHECK_INTERVAL);
});
}
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents(interval, callback) {
assert.strictEqual(typeof interval, 'number');
assert.strictEqual(typeof callback, 'function');
function processDockerEvents() {
// note that for some reason, the callback is called only on the first event
debug('Listening for docker events');
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
let lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
const since = ((new Date().getTime() / 1000) - interval).toFixed(0);
const until = ((new Date().getTime() / 1000) - 1).toFixed(0);
var lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return callback(error);
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return console.error(error);
gDockerEventStream = stream;
stream.setEncoding('utf8');
stream.on('data', function (data) {
@@ -150,48 +173,34 @@ function processDockerEvents(interval, callback) {
});
stream.on('error', function (error) {
debug('Error reading docker events', error);
callback();
console.error('Error reading docker events', error);
gDockerEventStream = null; // TODO: reconnect?
});
stream.on('end', callback);
// safety hatch if 'until' doesn't work (there are cases where docker is working with a different time)
setTimeout(stream.destroy.bind(stream), 3000); // https://github.com/apocas/dockerode/issues/179
stream.on('end', function () {
console.error('Docker event stream ended');
gDockerEventStream = null; // TODO: reconnect?
});
});
}
function processApp(callback) {
function start(callback) {
assert.strictEqual(typeof callback, 'function');
apps.getAll(function (error, result) {
if (error) return callback(error);
debug('Starting apphealthmonitor');
async.each(result, checkAppHealth, function (error) {
if (error) console.error(error);
processDockerEvents();
var alive = result
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
run();
debug('apps alive: [%s]', alive);
callback(null);
});
});
callback();
}
function run(interval, callback) {
assert.strictEqual(typeof interval, 'number');
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
callback = callback || NOOP_CALLBACK;
clearTimeout(gRunTimeout);
if (gDockerEventStream) gDockerEventStream.end();
async.series([
processDockerEvents.bind(null, interval),
processApp
], function (error) {
if (error) debug(error);
callback();
});
callback();
}
+122 -186
View File
@@ -45,13 +45,10 @@ exports = module.exports = {
setOwner: setOwner,
transferOwnership: transferOwnership,
PORT_TYPE_TCP: 'tcp',
PORT_TYPE_UDP: 'udp',
// exported for testing
_validateHostname: validateHostname,
_validatePortBindings: validatePortBindings,
_validateAccessRestriction: validateAccessRestriction,
_translatePortBindings: translatePortBindings
_validateAccessRestriction: validateAccessRestriction
};
var appdb = require('./appdb.js'),
@@ -84,6 +81,7 @@ var appdb = require('./appdb.js'),
split = require('split'),
superagent = require('superagent'),
taskmanager = require('./taskmanager.js'),
tld = require('tldjs'),
TransformStream = require('stream').Transform,
updateChecker = require('./updatechecker.js'),
url = require('url'),
@@ -125,10 +123,43 @@ AppsError.BILLING_REQUIRED = 'Billing Required';
AppsError.ACCESS_DENIED = 'Access denied';
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name (and not dns name)
function validateHostname(location, domain, hostname) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof hostname, 'string');
const RESERVED_LOCATIONS = [
constants.API_LOCATION,
constants.SMTP_LOCATION,
constants.IMAP_LOCATION
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
if (hostname === config.adminFqdn()) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
// workaround https://github.com/oncletom/tld.js/issues/73
var tmp = hostname.replace('_', '-');
if (!tld.isValid(tmp)) return new AppsError(AppsError.BAD_FIELD, 'Hostname is not a valid domain name');
if (hostname.length > 253) return new AppsError(AppsError.BAD_FIELD, 'Hostname length exceeds 253 characters');
if (location) {
// label validation
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new AppsError(AppsError.BAD_FIELD, 'Invalid subdomain length');
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new AppsError(AppsError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot');
if (/^[-.]/.test(location)) return new AppsError(AppsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot');
}
return null;
}
// validate the port bindings
function validatePortBindings(portBindings, manifest) {
function validatePortBindings(portBindings, tcpPorts) {
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof manifest, 'object');
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
@@ -159,58 +190,26 @@ function validatePortBindings(portBindings, manifest) {
if (!portBindings) return null;
for (let portName in portBindings) {
if (!/^[a-zA-Z0-9_]+$/.test(portName)) return new AppsError(AppsError.BAD_FIELD, `${portName} is not a valid environment variable`);
var env;
for (env in portBindings) {
if (!/^[a-zA-Z0-9_]+$/.test(env)) return new AppsError(AppsError.BAD_FIELD, env + ' is not valid environment variable');
const hostPort = portBindings[portName];
if (!Number.isInteger(hostPort)) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not an integer`);
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(hostPort));
if (hostPort <= 1023 || hostPort > 65535) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not in permitted range`);
if (!Number.isInteger(portBindings[env])) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not an integer');
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
if (portBindings[env] <= 1023 || portBindings[env] > 65535) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not in permitted range');
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
// that the user wants the service disabled
const tcpPorts = manifest.tcpPorts || { };
const udpPorts = manifest.udpPorts || { };
for (let portName in portBindings) {
if (!(portName in tcpPorts) && !(portName in udpPorts)) return new AppsError(AppsError.BAD_FIELD, `Invalid portBindings ${portName}`);
tcpPorts = tcpPorts || { };
for (env in portBindings) {
if (!(env in tcpPorts)) return new AppsError(AppsError.BAD_FIELD, 'Invalid portBindings ' + env);
}
return null;
}
function translatePortBindings(portBindings, manifest) {
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof manifest, 'object');
if (!portBindings) return null;
let result = {};
const tcpPorts = manifest.tcpPorts || { };
for (let portName in portBindings) {
const portType = portName in tcpPorts ? exports.PORT_TYPE_TCP : exports.PORT_TYPE_UDP;
result[portName] = { hostPort: portBindings[portName], type: portType };
}
return result;
}
function postProcess(app) {
let result = {};
for (let portName in app.portBindings) {
result[portName] = app.portBindings[portName].hostPort;
}
app.portBindings = result;
}
function addSpacesSuffix(location, user) {
if (user.admin || !config.isSpacesEnabled()) return location;
const spacesSuffix = user.username.replace(/\./g, '-');
return location === '' ? spacesSuffix : `${location}-${spacesSuffix}`;
}
function validateAccessRestriction(accessRestriction) {
assert.strictEqual(typeof accessRestriction, 'object');
@@ -291,16 +290,6 @@ function validateBackupFormat(format) {
return new AppsError(AppsError.BAD_FIELD, 'Invalid backup format');
}
function validateEnv(env) {
for (let key in env) {
if (key.length > 512) return new AppsError(AppsError.BAD_FIELD, 'Max env var key length is 512');
// http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) return new AppsError(AppsError.BAD_FIELD, `"${key}" is not a valid environment variable`);
}
return null;
}
function getDuplicateErrorDetails(location, portBindings, error) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
@@ -316,8 +305,8 @@ function getDuplicateErrorDetails(location, portBindings, error) {
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
// check if any of the port bindings conflict
for (let portName in portBindings) {
if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
for (var env in portBindings) {
if (portBindings[env] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
}
return new AppsError(AppsError.ALREADY_EXISTS);
@@ -335,8 +324,7 @@ function getAppConfig(app) {
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
robotsTxt: app.robotsTxt,
sso: app.sso,
alternateDomains: app.alternateDomains || [],
env: app.env
alternateDomains: app.alternateDomains || []
};
}
@@ -346,7 +334,7 @@ function removeInternalFields(app) {
'location', 'domain', 'fqdn', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts',
'alternateDomains', 'ownerId', 'env');
'alternateDomains', 'ownerId');
}
function removeRestrictedFields(app) {
@@ -383,21 +371,15 @@ function get(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
domaindb.getAll(function (error, domainObjects) {
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
domaindb.get(app.domain, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
postProcess(app);
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -414,38 +396,25 @@ function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
domaindb.getAll(function (error, domainObjects) {
docker.getContainerIdByIp(ip, function (error, containerId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
docker.getContainerIdByIp(ip, function (error, containerId) {
appdb.getByContainerId(containerId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.getByContainerId(containerId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
domaindb.get(app.domain, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
postProcess(app);
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
domaindb.getAll(function (error, domainObjects) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
if (!error) app.mailboxName = mailboxes[0].name;
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
callback(null, app);
});
});
});
@@ -455,34 +424,28 @@ function getByIpAddress(ip, callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.getAll(function (error, domainObjects) {
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
async.eachSeries(apps, function (app, iteratorDone) {
domaindb.get(app.domain, function (error, result) {
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
apps.forEach(postProcess);
async.eachSeries(apps, function (app, iteratorDone) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
iteratorDone(null, app);
});
}, function (error) {
if (error) return callback(error);
callback(null, apps);
});
}, function (error) {
if (error) return callback(error);
callback(null, apps);
});
});
}
@@ -524,9 +487,8 @@ function mailboxNameForLocation(location, manifest) {
return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
}
function install(data, user, auditSource, callback) {
function install(data, auditSource, callback) {
assert(data && typeof data === 'object');
assert(user && typeof user === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -545,9 +507,7 @@ function install(data, user, auditSource, callback) {
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
backupId = data.backupId || null,
backupFormat = data.backupFormat || 'tgz',
ownerId = data.ownerId,
alternateDomains = data.alternateDomains || [],
env = data.env || {};
ownerId = data.ownerId;
assert(data.appStoreId || data.manifest); // atleast one of them is required
@@ -560,7 +520,7 @@ function install(data, user, auditSource, callback) {
error = checkManifestConstraints(manifest);
if (error) return callback(error);
error = validatePortBindings(portBindings, manifest);
error = validatePortBindings(portBindings, manifest.tcpPorts);
if (error) return callback(error);
error = validateAccessRestriction(accessRestriction);
@@ -585,9 +545,6 @@ function install(data, user, auditSource, callback) {
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
error = validateEnv(env);
if (error) return callback(error);
var appId = uuid.v4();
if (icon) {
@@ -602,14 +559,13 @@ function install(data, user, auditSource, callback) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
location = addSpacesSuffix(location, user);
alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
var fqdn = domains.fqdn(location, domain, domainObject.provider);
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
error = validateHostname(location, domain, fqdn);
if (error) return callback(error);
if (cert && key) {
error = reverseProxy.validateCertificate(location, domainObject, { cert, key });
error = reverseProxy.validateCertificate(fqdn, cert, key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
@@ -624,21 +580,19 @@ function install(data, user, auditSource, callback) {
mailboxName: mailboxNameForLocation(location, manifest),
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
robotsTxt: robotsTxt,
alternateDomains: alternateDomains,
env: env
robotsTxt: robotsTxt
};
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appstore.purchase(appId, { appstoreId: appStoreId, manifestId: manifest.id }, function (appstoreError) {
appstore.purchase(appId, appStoreId, function (appstoreError) {
// if purchase failed, rollback the appdb record
if (appstoreError) {
appdb.del(appId, function (error) {
if (error) debug('install: Failed to rollback app installation.', error);
if (error) console.error('Failed to rollback app installation.', error);
if (appstoreError.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, appstoreError.message));
if (appstoreError && appstoreError.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, appstoreError.message));
@@ -652,8 +606,8 @@ function install(data, user, auditSource, callback) {
// save cert to boxdata/certs
if (cert && key) {
let error = reverseProxy.setAppCertificateSync(location, domainObject, { cert, key });
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error setting cert: ' + error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
taskmanager.restartAppTask(appId);
@@ -672,17 +626,16 @@ function install(data, user, auditSource, callback) {
});
}
function configure(appId, data, user, auditSource, callback) {
function configure(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert(data && typeof data === 'object');
assert(user && typeof user === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
let domain, location, portBindings, values = { }, mailboxName;
var domain, location, portBindings, values = { };
if ('location' in data) location = values.location = data.location.toLowerCase();
else location = app.location;
@@ -696,10 +649,9 @@ function configure(appId, data, user, auditSource, callback) {
}
if ('portBindings' in data) {
error = validatePortBindings(data.portBindings, app.manifest);
portBindings = values.portBindings = data.portBindings;
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
if (error) return callback(error);
values.portBindings = translatePortBindings(data.portBindings, app.manifest);
portBindings = data.portBindings;
} else {
portBindings = app.portBindings;
}
@@ -729,47 +681,36 @@ function configure(appId, data, user, auditSource, callback) {
}
if ('mailboxName' in data) {
if (data.mailboxName === '') { // special case to reset back to .app
mailboxName = mailboxNameForLocation(location, app.manifest);
} else {
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
mailboxName = data.mailboxName;
}
} else { // keep existing name or follow the new location
mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
}
if ('alternateDomains' in data) {
// TODO validate all subdomains [{ domain: '', subdomain: ''}]
values.alternateDomains = data.alternateDomains;
values.alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
}
if ('env' in data) {
values.env = data.env;
error = validateEnv(data.env);
if (error) return callback(error);
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
location = addSpacesSuffix(location, user);
var fqdn = domains.fqdn(location, domain, domainObject.provider);
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
error = validateHostname(location, domain, fqdn);
if (error) return callback(error);
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
error = reverseProxy.validateCertificate(location, domainObject, { cert: data.cert, key: data.key });
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
error = reverseProxy.setAppCertificateSync(location, domainObject, { cert: data.cert, key: data.key });
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error setting cert: ' + error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
} else { // remove existing cert/key
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`))) debug('Error removing key: ' + safe.error.message);
}
}
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
@@ -779,8 +720,10 @@ function configure(appId, data, user, auditSource, callback) {
debug('Will configure app with id:%s values:%j', appId, values);
// make the mailbox name follow the apps new location, if the user did not set it explicitly
mailboxdb.updateName(app.mailboxName /* old */, values.oldConfig.domain, mailboxName, domain, function (error) {
if (mailboxName.endsWith('.app')) error = null; // ignore internal mailbox conflict errors since we want to show location conflict errors in the UI
var oldName = app.mailboxName;
var newName = data.mailboxName || (app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName);
mailboxdb.updateName(oldName, values.oldConfig.domain, newName, domain, function (error) {
if (newName.endsWith('.app')) error = null; // ignore internal mailbox conflict errors since we want to show location conflict errors in the UI
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
@@ -884,7 +827,7 @@ function getLogs(appId, options, callback) {
debug('Getting logs for %s', appId);
get(appId, function (error, app) {
get(appId, function (error /*, app */) {
if (error) return callback(error);
var lines = options.lines || 100,
@@ -898,7 +841,6 @@ function getLogs(appId, options, callback) {
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
args.push(path.join(paths.LOG_DIR, appId, 'app.log'));
if (app.manifest.addons && app.manifest.addons.redis) args.push(path.join(paths.LOG_DIR, `redis-${appId}/app.log`));
var cp = spawn('/usr/bin/tail', args);
@@ -974,10 +916,9 @@ function restore(appId, data, auditSource, callback) {
});
}
function clone(appId, data, user, auditSource, callback) {
function clone(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert(user && typeof user === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1009,16 +950,15 @@ function clone(appId, data, user, auditSource, callback) {
error = checkManifestConstraints(backupInfo.manifest);
if (error) return callback(error);
error = validatePortBindings(portBindings, backupInfo.manifest);
error = validatePortBindings(portBindings, backupInfo.manifest.tcpPorts);
if (error) return callback(error);
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
location = addSpacesSuffix(location, user);
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
error = validateHostname(location, domain, domains.fqdn(location, domain, domainObject.provider));
if (error) return callback(error);
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
@@ -1031,19 +971,18 @@ function clone(appId, data, user, auditSource, callback) {
sso: !!app.sso,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt,
env: app.env
robotsTxt: app.robotsTxt
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appstore.purchase(newAppId, { appstoreId: app.appStoreId, manifestId: manifest.id }, function (appstoreError) {
appstore.purchase(newAppId, app.appStoreId, function (appstoreError) {
// if purchase failed, rollback the appdb record
if (appstoreError) {
appdb.del(newAppId, function (error) {
if (error) debug('install: Failed to rollback app installation.', error);
if (error) console.error('Failed to rollback app installation.', error);
if (appstoreError.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, appstoreError.message));
if (appstoreError && appstoreError.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, appstoreError.message));
@@ -1082,7 +1021,7 @@ function uninstall(appId, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
appstore.unpurchase(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id }, function (error) {
appstore.unpurchase(appId, app.appStoreId, function (error) {
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
@@ -1180,8 +1119,6 @@ function exec(appId, options, callback) {
};
container.exec(execOptions, function (error, exec) {
if (error && error.statusCode === 409) return callback(new AppsError(AppsError.BAD_STATE, error.message)); // container restarting/not running
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var startOptions = {
Detach: false,
@@ -1222,12 +1159,11 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
function canAutoupdateApp(app, newManifest) {
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
const newTcpPorts = newManifest.tcpPorts || { };
const newUdpPorts = newManifest.udpPorts || { };
const portBindings = app.portBindings; // this is never null
var newTcpPorts = newManifest.tcpPorts || { };
var portBindings = app.portBindings; // this is never null
for (let portName in portBindings) {
if (!(portName in newTcpPorts) && !(portName in newUdpPorts)) return new Error(`${portName} was in use but new update removes it`);
for (var env in portBindings) {
if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it');
}
// it's fine if one or more (unused) keys got removed
+42 -18
View File
@@ -19,7 +19,8 @@ exports = module.exports = {
AppstoreError: AppstoreError
};
var apps = require('./apps.js'),
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
@@ -94,36 +95,59 @@ function isFreePlan(subscription) {
return !subscription || subscription.plan.id === 'free';
}
// See app.js install it will create a db record first but remove it again if appstore purchase fails
function purchase(appId, data, callback) {
function purchase(appId, appstoreId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof appstoreId, 'string');
assert.strictEqual(typeof callback, 'function');
getAppstoreConfig(function (error, appstoreConfig) {
if (appstoreId === '') return callback(null);
function doThePurchase() {
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
var data = { appstoreId: appstoreId };
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
callback(null);
});
});
}
getSubscription(function (error, result) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
// only check for app install count if on the free plan
if (result.id !== 'free') return doThePurchase();
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
appdb.getAppStoreIds(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
callback(null);
var count = result.filter(function (a) { return !!a.appStoreId; }).length;
// we only allow max of 2 app installations without a subscription
// WARNING install and clone in apps.js will first add the db record and then call purchase() so we test for more than 2 here
if (count > 2) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, 'Too many apps installed'));
doThePurchase();
});
});
}
function unpurchase(appId, data, callback) {
function unpurchase(appId, appstoreId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof appstoreId, 'string');
assert.strictEqual(typeof callback, 'function');
if (appstoreId === '') return callback(null);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
@@ -135,7 +159,7 @@ function unpurchase(appId, data, callback) {
if (result.statusCode === 404) return callback(null); // was never purchased
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
superagent.del(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode !== 204) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
+35 -89
View File
@@ -10,8 +10,8 @@ exports = module.exports = {
_reserveHttpPort: reserveHttpPort,
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_createAppDir: createAppDir,
_deleteAppDir: deleteAppDir,
_createVolume: createVolume,
_deleteVolume: deleteVolume,
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
@@ -36,7 +36,6 @@ var addons = require('./addons.js'),
ejs = require('ejs'),
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
mkdirp = require('mkdirp'),
net = require('net'),
os = require('os'),
path = require('path'),
@@ -47,13 +46,16 @@ var addons = require('./addons.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh'),
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh'),
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -133,20 +135,6 @@ function createContainer(app, callback) {
});
}
// Only delete the main container of the app, not destroy any docker addon created ones
function deleteMainContainer(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'deleting main app container');
docker.deleteContainer(app.containerId, function (error) {
if (error) return callback(new Error('Error deleting container: ' + error));
updateApp(app, { containerId: null }, callback);
});
}
function deleteContainers(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -160,47 +148,19 @@ function deleteContainers(app, callback) {
});
}
function createAppDir(app, callback) {
function createVolume(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
mkdirp(path.join(paths.APPS_DATA_DIR, app.id), callback);
shell.sudo('createVolume', [ CREATEAPPDIR_CMD, app.id ], callback);
}
function deleteAppDir(app, options, callback) {
function deleteVolume(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
const appDataDir = path.join(paths.APPS_DATA_DIR, app.id);
// resolve any symlinked data dir
const stat = safe.fs.lstatSync(appDataDir);
if (!stat) return callback(null);
const resolvedAppDataDir = stat.isSymbolicLink() ? safe.fs.readlinkSync(appDataDir) : appDataDir;
if (safe.fs.existsSync(resolvedAppDataDir)) {
const entries = safe.fs.readdirSync(resolvedAppDataDir);
if (!entries) return callback(`Error listing ${resolvedAppDataDir}: ${safe.error.message}`);
// remove only files. directories inside app dir are currently volumes managed by the addons
entries.forEach(function (entry) {
let stat = safe.fs.statSync(path.join(resolvedAppDataDir, entry));
if (stat && !stat.isDirectory()) safe.fs.unlinkSync(path.join(resolvedAppDataDir, entry));
});
}
// if this fails, it's probably because the localstorage/redis addons have not cleaned up properly
if (options.removeDirectory) {
if (stat.isSymbolicLink()) {
if (!safe.fs.unlinkSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error);
} else {
if (!safe.fs.rmdirSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error);
}
}
callback(null);
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id, !!options.removeDirectory ], callback);
}
function addCollectdProfile(app, callback) {
@@ -310,10 +270,7 @@ function registerSubdomain(app, overwrite, callback) {
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
debug('Upsert error. Will retry.', error.message);
return retryCallback(error); // try again
}
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
@@ -370,10 +327,8 @@ function registerAlternateDomains(app, overwrite, callback) {
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
debug('Upsert error. Will retry.', error.message);
return retryCallback(error); // try again
}
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
});
@@ -390,14 +345,9 @@ function unregisterAlternateDomains(app, all, callback) {
assert.strictEqual(typeof all, 'boolean');
assert.strictEqual(typeof callback, 'function');
let obsoleteDomains = [];
if (all) {
obsoleteDomains = app.alternateDomains;
} else if (app.oldConfig) { // oldConfig can be null during an infra update
obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) {
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
});
}
var obsoleteDomains
if (all) obsoleteDomains = app.alternateDomains;
else obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) { return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; }); });
if (obsoleteDomains.length === 0) return callback();
@@ -436,10 +386,8 @@ function cleanupLogs(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// note that redis container logs are cleaned up by the addon
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
callback(null);
});
}
@@ -456,12 +404,13 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 240 }, function (error) {
if (error) return callback(error);
// now wait for alternateDomains, if any
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, iteratorCallback);
async.eachSeries(app.alternateDomains, function (domain, callback) {
var fqdn = (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain;
domains.waitForDnsRecord(fqdn, domain.domain, ip, { interval: 5000, times: 240 }, callback);
}, callback);
});
});
@@ -495,14 +444,16 @@ function install(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteMainContainer.bind(null, app),
deleteContainers.bind(null, app),
function teardownAddons(next) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
var addonsToRemove = !isRestoring ? app.manifest.addons : _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
var addonsToRemove = !isRestoring
? app.manifest.addons
: _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
addons.teardownAddons(app, addonsToRemove, next);
},
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
// for restore case
function deleteImageIfChanged(done) {
@@ -519,14 +470,11 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
registerSubdomain.bind(null, app, isRestoring /* overwrite */),
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
registerAlternateDomains.bind(null, app, isRestoring /* overwrite */),
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '50, Creating volume' }),
createAppDir.bind(null, app),
createVolume.bind(null, app),
function restoreFromBackup(next) {
if (!restoreConfig) {
@@ -537,8 +485,6 @@ function install(app, callback) {
} else {
async.series([
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
addons.clearAddons.bind(null, app, app.manifest.addons),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig),
], next);
}
@@ -611,7 +557,7 @@ function configure(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteMainContainer.bind(null, app),
deleteContainers.bind(null, app),
unregisterAlternateDomains.bind(null, app, false /* all */),
function (next) {
if (!locationChanged) return next();
@@ -635,7 +581,7 @@ function configure(app, callback) {
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '45, Ensuring volume' }),
createAppDir.bind(null, app),
createVolume.bind(null, app),
// re-setup addons since they rely on the app's fqdn (e.g oauth)
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
@@ -712,7 +658,7 @@ function update(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteMainContainer.bind(null, app),
deleteContainers.bind(null, app),
function deleteImageIfChanged(done) {
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
@@ -724,14 +670,14 @@ function update(app, callback) {
// free unused ports
function (next) {
const currentPorts = app.portBindings || {};
const newTcpPorts = app.updateConfig.manifest.tcpPorts || {};
const newUdpPorts = app.updateConfig.manifest.udpPorts || {};
// make sure we always have objects
var currentPorts = app.portBindings || {};
var newPorts = app.updateConfig.manifest.tcpPorts || {};
async.each(Object.keys(currentPorts), function (portName, callback) {
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(); // port still in use
if (newPorts[portName]) return callback(); // port still in use
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
appdb.delPortBinding(currentPorts[portName], function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.');
else if (error) return next(error);
@@ -804,7 +750,7 @@ function uninstall(app, callback) {
addons.teardownAddons.bind(null, app, app.manifest.addons),
updateApp.bind(null, app, { installationProgress: '40, Deleting volume' }),
deleteAppDir.bind(null, app, { removeDirectory: true }),
deleteVolume.bind(null, app, { removeDirectory: true }),
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
docker.deleteImage.bind(null, app.manifest),
+6 -17
View File
@@ -124,9 +124,6 @@ function testConfig(backupConfig, callback) {
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BackupsError(BackupsError.BAD_FIELD, 'unknown format'));
// remember to adjust the cron ensureBackup task interval accordingly
if (backupConfig.intervalSecs < 6 * 60 * 60) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Interval must be atleast 6 hours'));
api(backupConfig.provider).testConfig(backupConfig, callback);
}
@@ -565,13 +562,9 @@ function restore(backupConfig, backupId, callback) {
download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, function (error) {
if (error) return callback(error);
debug('restore: download completed, importing database');
database.importFromFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug('restore: database imported');
callback();
});
});
@@ -988,19 +981,15 @@ function ensureBackup(auditSource, callback) {
getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, backups) {
if (error) {
debug('Unable to list backups', error);
return callback(error);
return callback(error); // no point trying to backup if appstore is down
}
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
debug('Previous backup was %j, no need to backup now', backups[0]);
return callback(null);
}
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < (backupConfig.intervalSecs - 3600) * 1000)) { // adjust 1 hour
debug('Previous backup was %j, no need to backup now', backups[0]);
return callback(null);
}
backup(auditSource, callback);
});
backup(auditSource, callback);
});
}
+62 -64
View File
@@ -3,7 +3,7 @@
var assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme1'),
debug = require('debug')('box:cert/acme'),
execSync = require('safetydance').child_process.execSync,
fs = require('fs'),
parseLinks = require('parse-links'),
@@ -25,7 +25,7 @@ exports = module.exports = {
_name: 'acme'
};
function Acme1Error(reason, errorOrMessage) {
function AcmeError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -43,18 +43,18 @@ function Acme1Error(reason, errorOrMessage) {
this.nestedError = errorOrMessage;
}
}
util.inherits(Acme1Error, Error);
Acme1Error.INTERNAL_ERROR = 'Internal Error';
Acme1Error.EXTERNAL_ERROR = 'External Error';
Acme1Error.ALREADY_EXISTS = 'Already Exists';
Acme1Error.NOT_COMPLETED = 'Not Completed';
Acme1Error.FORBIDDEN = 'Forbidden';
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 Acme1(options) {
function Acme(options) {
assert.strictEqual(typeof options, 'object');
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
@@ -62,7 +62,7 @@ function Acme1(options) {
this.email = options.email;
}
Acme1.prototype.getNonce = function (callback) {
Acme.prototype.getNonce = function (callback) {
superagent.get(this.caOrigin + '/directory').timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
@@ -91,7 +91,7 @@ function getModulus(pem) {
return Buffer.from(match[1], 'hex');
}
Acme1.prototype.sendSignedRequest = function (url, payload, callback) {
Acme.prototype.sendSignedRequest = function (url, payload, callback) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -136,7 +136,7 @@ Acme1.prototype.sendSignedRequest = function (url, payload, callback) {
});
};
Acme1.prototype.updateContact = function (registrationUri, callback) {
Acme.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -151,8 +151,8 @@ Acme1.prototype.updateContact = function (registrationUri, callback) {
var that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
debug('updateContact: contact of user updated to %s', that.email);
@@ -160,7 +160,7 @@ Acme1.prototype.updateContact = function (registrationUri, callback) {
});
};
Acme1.prototype.registerUser = function (callback) {
Acme.prototype.registerUser = function (callback) {
assert.strictEqual(typeof callback, 'function');
var payload = {
@@ -173,9 +173,9 @@ Acme1.prototype.registerUser = function (callback) {
var that = this;
this.sendSignedRequest(this.caOrigin + '/acme/new-reg', JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
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', that.email);
@@ -183,7 +183,7 @@ Acme1.prototype.registerUser = function (callback) {
});
};
Acme1.prototype.registerDomain = function (domain, callback) {
Acme.prototype.registerDomain = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -198,9 +198,9 @@ Acme1.prototype.registerDomain = function (domain, callback) {
debug('registerDomain: %s', domain);
this.sendSignedRequest(this.caOrigin + '/acme/new-authz', JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new Acme1Error(Acme1Error.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
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);
@@ -208,7 +208,7 @@ Acme1.prototype.registerDomain = function (domain, callback) {
});
};
Acme1.prototype.prepareHttpChallenge = function (challenge, callback) {
Acme.prototype.prepareHttpChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -232,13 +232,13 @@ Acme1.prototype.prepareHttpChallenge = function (challenge, callback) {
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 Acme1Error(Acme1Error.INTERNAL_ERROR, error));
if (error) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, error));
callback();
});
};
Acme1.prototype.notifyChallengeReady = function (challenge, callback) {
Acme.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -252,14 +252,14 @@ Acme1.prototype.notifyChallengeReady = function (challenge, callback) {
};
this.sendSignedRequest(challenge.uri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
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();
});
};
Acme1.prototype.waitForChallenge = function (challenge, callback) {
Acme.prototype.waitForChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -271,18 +271,18 @@ Acme1.prototype.waitForChallenge = function (challenge, callback) {
superagent.get(challenge.uri).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForChallenge: network error getting uri %s', challenge.uri);
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, error.message)); // network error
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 Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new Acme1Error(Acme1Error.NOT_COMPLETED));
if (result.body.status === 'pending') return retryCallback(new AcmeError(AcmeError.NOT_COMPLETED));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
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()
@@ -291,7 +291,7 @@ Acme1.prototype.waitForChallenge = function (challenge, callback) {
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
Acme1.prototype.signCertificate = function (domain, csrDer, callback) {
Acme.prototype.signCertificate = function (domain, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
@@ -306,13 +306,13 @@ Acme1.prototype.signCertificate = function (domain, csrDer, callback) {
debug('signCertificate: sending new-cert request');
this.sendSignedRequest(this.caOrigin + '/acme/new-cert', JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
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 Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
if (!certUrl) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // maybe use for renewal
@@ -320,7 +320,7 @@ Acme1.prototype.signCertificate = function (domain, csrDer, callback) {
});
};
Acme1.prototype.createKeyAndCsr = function (domain, callback) {
Acme.prototype.createKeyAndCsr = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -333,15 +333,15 @@ Acme1.prototype.createKeyAndCsr = function (domain, callback) {
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
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 Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error)); // bookkeeping
if (!csrDer) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
@@ -349,13 +349,13 @@ Acme1.prototype.createKeyAndCsr = function (domain, callback) {
};
// TODO: download the chain in a loop following 'up' header
Acme1.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
Acme.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
debug('downloadChain: linkHeader %s', linkHeader);
var linkInfo = parseLinks(linkHeader);
if (!linkInfo || !linkInfo.up) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
if (!linkInfo || !linkInfo.up) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
var intermediateCertUrl = linkInfo.up.startsWith('https://') ? linkInfo.up : (this.caOrigin + linkInfo.up);
@@ -366,18 +366,18 @@ Acme1.prototype.downloadChain = function (linkHeader, callback) {
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
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 chainDer = result.text;
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
if (!chainPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
callback(null, chainPem);
});
};
Acme1.prototype.downloadCertificate = function (domain, certUrl, callback) {
Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof certUrl, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -390,9 +390,9 @@ Acme1.prototype.downloadCertificate = function (domain, certUrl, callback) {
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
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;
@@ -400,14 +400,14 @@ Acme1.prototype.downloadCertificate = function (domain, certUrl, callback) {
debug('downloadCertificate: cert der file for %s saved', domain);
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
if (!certificatePem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!certificatePem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
that.downloadChain(result.header['link'], function (error, chainPem) {
if (error) return callback(error);
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
@@ -416,14 +416,14 @@ Acme1.prototype.downloadCertificate = function (domain, certUrl, callback) {
});
};
Acme1.prototype.acmeFlow = function (domain, callback) {
Acme.prototype.acmeFlow = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!this.accountKeyPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
@@ -441,7 +441,7 @@ Acme1.prototype.acmeFlow = function (domain, callback) {
debug('acmeFlow: challenges: %j', result);
var httpChallenges = result.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'no http challenges'));
if (httpChallenges.length === 0) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'no http challenges'));
var challenge = httpChallenges[0];
async.waterfall([
@@ -456,26 +456,24 @@ Acme1.prototype.acmeFlow = function (domain, callback) {
});
};
Acme1.prototype.getCertificate = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
Acme.prototype.getCertificate = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: start acme flow for %s from %s', hostname, this.caOrigin);
this.acmeFlow(hostname, function (error) {
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
this.acmeFlow(domain, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
callback(null, path.join(outdir, hostname + '.cert'), path.join(outdir, hostname + '.key'));
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
});
};
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme1(options || { });
acme.getCertificate(hostname, domain, callback);
var acme = new Acme(options || { });
acme.getCertificate(domain, callback);
}
-637
View File
@@ -1,637 +0,0 @@
'use strict';
var assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('../domains.js'),
execSync = require('safetydance').child_process.execSync,
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
CA_STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'acme',
_getChallengeSubdomain: getChallengeSubdomain
};
function Acme2Error(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(Acme2Error, Error);
Acme2Error.INTERNAL_ERROR = 'Internal Error';
Acme2Error.EXTERNAL_ERROR = 'External Error';
Acme2Error.ALREADY_EXISTS = 'Already Exists';
Acme2Error.NOT_COMPLETED = 'Not Completed';
Acme2Error.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 Acme2(options) {
assert.strictEqual(typeof options, 'object');
this.accountKeyPem = null; // Buffer
this.email = options.email;
this.keyId = null;
this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
this.directory = {};
this.performHttpAuthorization = !!options.performHttpAuthorization;
this.wildcard = !!options.wildcard;
}
Acme2.prototype.getNonce = function (callback) {
superagent.get(this.directory.newNonce).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 204) 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 getModulus(pem) {
assert(util.isBuffer(pem));
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
return Buffer.from(match[1], 'hex');
}
Acme2.prototype.sendSignedRequest = function (url, payload, callback) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
assert.strictEqual(typeof callback, 'function');
assert(util.isBuffer(this.accountKeyPem));
const that = this;
let header = {
url: url,
alg: 'RS256'
};
// keyId is null when registering account
if (this.keyId) {
header.kid = this.keyId;
} else {
header.jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
}
var payload64 = b64(payload);
this.getNonce(function (error, nonce) {
if (error) return callback(error);
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
var signer = crypto.createSign('RSA-SHA256');
signer.update(protected64 + '.' + payload64, 'utf8');
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
var data = {
protected: protected64,
payload: payload64,
signature: signature64
};
superagent.post(url).set('Content-Type', 'application/jose+json').set('User-Agent', 'acme-cloudron').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
if (error && !error.response) return callback(error); // network errors
callback(null, res);
});
});
};
Acme2.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`updateContact: registrationUri: ${registrationUri} email: ${this.email}`);
// https://github.com/ietf-wg-acme/acme/issues/30
const payload = {
contact: [ 'mailto:' + this.email ]
};
const that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
debug(`updateContact: contact of user updated to ${that.email}`);
callback();
});
};
Acme2.prototype.registerUser = function (callback) {
assert.strictEqual(typeof callback, 'function');
var payload = {
termsOfServiceAgreed: true
};
debug('registerUser: registering user');
var that = this;
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering new account: ' + error.message));
// 200 if already exists. 201 for new accounts
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
debug(`registerUser: user registered keyid: ${result.headers.location}`);
that.keyId = result.headers.location;
that.updateContact(result.headers.location, callback);
});
};
Acme2.prototype.newOrder = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var payload = {
identifiers: [{
type: 'dns',
value: domain
}]
};
debug('newOrder: %s', domain);
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new Acme2Error(Acme2Error.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('newOrder: created order %s %j', domain, result.body);
const order = result.body, orderUrl = result.headers.location;
if (!Array.isArray(order.authorizations)) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid authorizations in order'));
if (typeof order.finalize !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid finalize in order'));
if (typeof orderUrl !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid order location in order header'));
callback(null, order, orderUrl);
});
};
Acme2.prototype.waitForOrder = function (orderUrl, callback) {
assert.strictEqual(typeof orderUrl, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`waitForOrder: ${orderUrl}`);
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
debug('waitForOrder: getting status');
superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForOrder: network error getting uri %s', orderUrl);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 200) {
debug('waitForOrder: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
else if (result.body.status === 'valid' && result.body.certificate) return retryCallback(null, result.body.certificate);
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
});
}, callback);
};
Acme2.prototype.getKeyAuthorization = function (token) {
assert(util.isBuffer(this.accountKeyPem));
let jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
let shasum = crypto.createHash('sha256');
shasum.update(JSON.stringify(jwk));
let thumbprint = urlBase64Encode(shasum.digest('base64'));
return token + '.' + thumbprint;
};
Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object'); // { type, status, url, token }
assert.strictEqual(typeof callback, 'function');
debug('notifyChallengeReady: %s was met', challenge.url);
const keyAuthorization = this.getKeyAuthorization(challenge.token);
var payload = {
resource: 'challenge',
keyAuthorization: keyAuthorization
};
this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
callback();
});
};
Acme2.prototype.waitForChallenge = function (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.url).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForChallenge: network error getting uri %s', challenge.url);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 200) {
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new Acme2Error(Acme2Error.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
Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof finalizationUrl, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
const payload = {
csr: b64(csrDer)
};
debug('signCertificate: sending sign request');
this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
return callback(null);
});
};
Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
const certName = hostname.replace('*.', '_.');
var csrFile = path.join(outdir, `${certName}.csr`);
var privateKeyFile = path.join(outdir, `${certName}.key`);
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
if (!csrDer) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
assert.strictEqual(typeof hostname, '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(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
const fullChainPem = result.text;
const certName = hostname.replace('*.', '_.');
var certificateFile = path.join(outdir, `${certName}.cert`);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
callback();
});
};
Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof callback, 'function');
debug('acmeFlow: challenges: %j', authorization);
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no http challenges'));
let challenge = httpChallenges[0];
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
let keyAuthorization = this.getKeyAuthorization(challenge.token);
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, error));
callback(null, challenge);
});
};
Acme2.prototype.cleanupHttpChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('cleanupHttpChallenge: unlinking %s', path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
fs.unlink(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), callback);
};
function getChallengeSubdomain(hostname, domain) {
let challengeSubdomain;
if (hostname === domain) {
challengeSubdomain = '_acme-challenge';
} else if (hostname.includes('*')) { // wildcard
let subdomain = hostname.slice(0, -domain.length - 1);
challengeSubdomain = subdomain ? subdomain.replace('*', '_acme-challenge') : '_acme-challenge';
} else {
challengeSubdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1);
}
debug(`getChallengeSubdomain: challenge subdomain for hostname ${hostname} at domain ${domain} is ${challengeSubdomain}`);
return challengeSubdomain;
}
Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof callback, 'function');
debug('acmeFlow: challenges: %j', authorization);
let dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
if (dnsChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no dns challenges'));
let challenge = dnsChallenges[0];
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
callback(null, challenge);
});
});
};
Acme2.prototype.cleanupDnsChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error));
callback(null);
});
};
Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorizationUrl, 'string');
assert.strictEqual(typeof callback, 'function');
const that = this;
superagent.get(authorizationUrl).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code getting authorization : ' + response.statusCode));
const authorization = response.body;
if (that.performHttpAuthorization) {
that.prepareHttpChallenge(hostname, domain, authorization, callback);
} else {
that.prepareDnsChallenge(hostname, domain, authorization, callback);
}
});
};
Acme2.prototype.cleanupChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
if (this.performHttpAuthorization) {
this.cleanupHttpChallenge(hostname, domain, challenge, callback);
} else {
this.cleanupDnsChallenge(hostname, domain, challenge, callback);
}
};
Acme2.prototype.acmeFlow = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
debug('getCertificate: using existing acme account key');
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
}
var that = this;
this.registerUser(function (error) {
if (error) return callback(error);
that.newOrder(hostname, function (error, order, orderUrl) {
if (error) return callback(error);
async.eachSeries(order.authorizations, function (authorizationUrl, iteratorCallback) {
debug(`acmeFlow: authorizing ${authorizationUrl}`);
that.prepareChallenge(hostname, domain, authorizationUrl, function (error, challenge) {
if (error) return iteratorCallback(error);
async.waterfall([
that.notifyChallengeReady.bind(that, challenge),
that.waitForChallenge.bind(that, challenge),
that.createKeyAndCsr.bind(that, hostname),
that.signCertificate.bind(that, hostname, order.finalize),
that.waitForOrder.bind(that, orderUrl),
that.downloadCertificate.bind(that, hostname)
], function (error) {
that.cleanupChallenge(hostname, domain, challenge, function (cleanupError) {
if (cleanupError) debug('acmeFlow: ignoring error when cleaning up challenge:', cleanupError);
iteratorCallback(error);
});
});
});
}, callback);
});
});
};
Acme2.prototype.getDirectory = function (callback) {
const that = this;
superagent.get(this.caDirectory).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching directory : ' + response.statusCode));
if (typeof response.body.newNonce !== 'string' ||
typeof response.body.newOrder !== 'string' ||
typeof response.body.newAccount !== 'string') return callback(new Error(`Invalid response body : ${response.body}`));
that.directory = response.body;
callback(null);
});
};
Acme2.prototype.getCertificate = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`getCertificate: start acme flow for ${hostname} from ${this.caDirectory}`);
if (hostname !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
hostname = domains.makeWildcard(hostname);
debug(`getCertificate: will get wildcard cert for ${hostname}`);
}
const that = this;
this.getDirectory(function (error) {
if (error) return callback(error);
that.acmeFlow(hostname, domain, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
const certName = hostname.replace('*.', '_.');
callback(null, path.join(outdir, `${certName}.cert`), path.join(outdir, `${certName}.key`));
});
});
};
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme2(options || { });
acme.getCertificate(hostname, domain, callback);
}
+3 -4
View File
@@ -10,13 +10,12 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', hostname);
debug('getCertificate: using fallback certificate', vhost);
return callback(null, '', '');
}
+3 -4
View File
@@ -10,13 +10,12 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/fallback.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', hostname);
debug('getCertificate: using fallback certificate', vhost);
return callback(null, '', '');
}
+1 -2
View File
@@ -12,8 +12,7 @@ exports = module.exports = {
var assert = require('assert');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
+8 -21
View File
@@ -68,7 +68,7 @@ ClientsError.NOT_FOUND = 'Not found';
ClientsError.INTERNAL_ERROR = 'Internal Error';
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
function validateClientName(name) {
function validateName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length < 1) return new ClientsError(ClientsError.BAD_FIELD, 'Name must be atleast 1 character');
@@ -79,14 +79,6 @@ function validateClientName(name) {
return null;
}
function validateTokenName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length > 64) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
return null;
}
function add(appId, type, redirectURI, scope, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
@@ -97,7 +89,7 @@ function add(appId, type, redirectURI, scope, callback) {
var error = accesscontrol.validateScopeString(scope);
if (error) return callback(new ClientsError(ClientsError.INVALID_SCOPE, error.message));
error = validateClientName(appId);
error = validateName(appId);
if (error) return callback(error);
var id = 'cid-' + uuid.v4();
@@ -252,17 +244,12 @@ function delByAppIdAndType(appId, type, callback) {
});
}
function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
function addTokenByUserId(clientId, userId, expiresAt, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof expiresAt, 'number');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
const name = options.name || '';
let error = validateTokenName(name);
if (error) return callback(error);
get(clientId, function (error, result) {
if (error) return callback(error);
@@ -278,7 +265,7 @@ function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
var token = tokendb.generateToken();
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), name, function (error) {
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), function (error) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
@@ -295,17 +282,17 @@ function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
}
// this issues a cid-cli token that does not require a password in various routes
function issueDeveloperToken(userObject, auditSource, callback) {
function issueDeveloperToken(userObject, ip, callback) {
assert.strictEqual(typeof userObject, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
const expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
addTokenByUserId('cid-cli', userObject.id, expiresAt, {}, function (error, result) {
addTokenByUserId('cid-cli', userObject.id, expiresAt, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: userObject.id, user: users.removePrivateFields(userObject) });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'cli', ip: ip }, { userId: userObject.id, user: users.removePrivateFields(userObject) });
callback(null, result);
});
+17 -116
View File
@@ -8,17 +8,12 @@ exports = module.exports = {
getConfig: getConfig,
getDisks: getDisks,
getLogs: getLogs,
getStatus: getStatus,
reboot: reboot,
onActivated: onActivated,
checkDiskSpace: checkDiskSpace,
configureWebadmin: configureWebadmin,
getWebadminStatus: getWebadminStatus
checkDiskSpace: checkDiskSpace
};
var assert = require('assert'),
@@ -26,7 +21,6 @@ var assert = require('assert'),
config = require('./config.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
domains = require('./domains.js'),
df = require('@sindresorhus/df'),
mailer = require('./mailer.js'),
os = require('os'),
@@ -40,7 +34,6 @@ var assert = require('assert'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
sysinfo = require('./sysinfo.js'),
users = require('./users.js'),
util = require('util');
@@ -48,16 +41,6 @@ var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
let gWebadminStatus = {
dns: false,
tls: false,
configuring: false,
restore: {
active: false,
error: null
}
};
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -88,46 +71,35 @@ CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
cron.startPreActivationJobs(callback);
runStartupTasks();
async.series([
settings.initialize,
reverseProxy.configureDefaultServer,
cron.initialize, // required for caas heartbeat before activation
onActivated
], callback);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
cron.stopJobs,
platform.stop
cron.uninitialize,
platform.stop,
settings.uninitialize
], callback);
}
function onActivated(callback) {
assert.strictEqual(typeof callback, 'function');
callback = callback || NOOP_CALLBACK;
// Starting the platform after a user is available means:
// 1. mail bounces can now be sent to the cloudron owner
// 2. the restore code path can run without sudo (since mail/ is non-root)
async.series([
platform.start,
cron.startPostActivationJobs
], callback);
}
users.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (!count) return callback(); // not activated
// each of these tasks can fail. we will add some routes to fix/re-run them
function runStartupTasks() {
// configure nginx to be reachable by IP
reverseProxy.configureDefaultServer(NOOP_CALLBACK);
// always generate webadmin config since we have no versioning mechanism for the ejs
configureWebadmin(NOOP_CALLBACK);
// check activation state and start the platform
users.isActivated(function (error, activated) {
if (error) return debug(error);
if (!activated) return debug('initialize: not activated yet'); // not activated
onActivated(NOOP_CALLBACK);
platform.start(callback);
});
}
@@ -173,10 +145,10 @@ function getConfig(callback) {
version: config.version(),
progress: progress.getAll(),
isDemo: config.isDemo(),
edition: config.edition(),
memory: os.totalmem(),
provider: config.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY]
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
spaces: allSettings[settings.SPACES_CONFIG_KEY] // here because settings route cannot be accessed by spaces users
});
});
}
@@ -294,74 +266,3 @@ function getLogs(unit, options, callback) {
return callback(null, transformStream);
}
function configureWebadmin(callback) {
assert.strictEqual(typeof callback, 'function');
debug('configureWebadmin: adminDomain:%s status:%j', config.adminDomain(), gWebadminStatus);
if (process.env.BOX_ENV === 'test' || !config.adminDomain() || gWebadminStatus.configuring) return callback();
gWebadminStatus.configuring = true; // re-entracy guard
function configureReverseProxy(error) {
debug('configureReverseProxy: error %j', error || null);
reverseProxy.configureAdmin({ userId: null, username: 'setup' }, function (error) {
debug('configureWebadmin: done error: %j', error || {});
gWebadminStatus.configuring = false;
if (error) return callback(error);
gWebadminStatus.tls = true;
callback();
});
}
// update the DNS. configure nginx regardless of whether it succeeded so that
// box is accessible even if dns creds are invalid
sysinfo.getPublicIp(function (error, ip) {
if (error) return configureReverseProxy(error);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
debug('addWebadminDnsRecord: updated records with error:', error);
if (error) return configureReverseProxy(error);
domains.waitForDnsRecord(config.adminLocation(), config.adminDomain(), 'A', ip, { interval: 30000, times: 50000 }, function (error) {
if (error) return configureReverseProxy(error);
gWebadminStatus.dns = true;
configureReverseProxy();
});
});
});
}
function getWebadminStatus() {
return gWebadminStatus;
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
activated: activated,
edition: config.edition(),
webadminStatus: gWebadminStatus // only valid when !activated
});
});
});
}
+16 -25
View File
@@ -22,8 +22,8 @@ exports = module.exports = {
setAdminFqdn: setAdminFqdn,
setAdminLocation: setAdminLocation,
version: version,
setVersion: setVersion,
database: database,
edition: edition,
// these values are derived
adminOrigin: adminOrigin,
@@ -36,12 +36,8 @@ exports = module.exports = {
hasIPv6: hasIPv6,
dkimSelector: dkimSelector,
isManaged: isManaged,
isDemo: isDemo,
// feature flags based on editions (these have a separate license from standard edition)
isSpacesEnabled: isSpacesEnabled,
// for testing resets to defaults
_reset: _reset
};
@@ -56,28 +52,31 @@ var assert = require('assert'),
// assert on unknown environment can't proceed
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
var data = { };
function baseDir() {
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
if (exports.CLOUDRON) return homeDir;
if (exports.TEST) return path.join(homeDir, '.cloudron_test');
// cannot reach
}
const cloudronConfigFileName = exports.CLOUDRON ? '/etc/cloudron/cloudron.conf' : path.join(baseDir(), 'cloudron.conf');
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
// only tests can run without a config file on disk, they use the defaults with runtime overrides
if (exports.CLOUDRON) assert(fs.existsSync(cloudronConfigFileName), 'No cloudron.conf found, cannot proceed');
function saveSync() {
// only save values we want to have in the cloudron.conf, see start.sh
var conf = {
version: data.version,
apiServerOrigin: data.apiServerOrigin,
webServerOrigin: data.webServerOrigin,
adminDomain: data.adminDomain,
adminFqdn: data.adminFqdn,
adminLocation: data.adminLocation,
provider: data.provider,
isDemo: data.isDemo,
edition: data.edition
isDemo: data.isDemo
};
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
@@ -97,14 +96,13 @@ function initConfig() {
data.adminDomain = '';
data.adminLocation = 'my';
data.port = 3000;
data.version = null;
data.apiServerOrigin = null;
data.webServerOrigin = null;
data.provider = 'generic';
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.dockerProxyPort = 3003;
data.edition = '';
// keep in sync with start.sh
data.database = {
@@ -117,6 +115,7 @@ function initConfig() {
// overrides for local testings
if (exports.TEST) {
data.version = '1.1.1-test';
data.port = 5454;
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
data.database.password = '';
@@ -205,8 +204,11 @@ function sysadminOrigin() {
}
function version() {
if (exports.TEST) return '3.0.0-test';
return fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim();
return get('version');
}
function setVersion(version) {
set('version', version);
}
function database() {
@@ -217,18 +219,10 @@ function isDemo() {
return get('isDemo') === true;
}
function isSpacesEnabled() {
return get('edition') === 'education';
}
function provider() {
return get('provider');
}
function isManaged() {
return edition() === 'hostingprovider';
}
function hasIPv6() {
const IPV6_PROC_FILE = '/proc/net/if_inet6';
return fs.existsSync(IPV6_PROC_FILE);
@@ -241,6 +235,3 @@ function dkimSelector() {
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
}
function edition() {
return get('edition');
}
+2 -1
View File
@@ -19,8 +19,9 @@ exports = module.exports = {
ADMIN_NAME: 'Settings',
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
+21 -37
View File
@@ -1,14 +1,11 @@
'use strict';
exports = module.exports = {
startPostActivationJobs: startPostActivationJobs,
startPreActivationJobs: startPreActivationJobs,
stopJobs: stopJobs
initialize: initialize,
uninitialize: uninitialize
};
var appHealthMonitor = require('./apphealthmonitor.js'),
apps = require('./apps.js'),
var apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
backups = require('./backups.js'),
@@ -43,9 +40,8 @@ var gJobs = {
cleanupTokens: null,
digestEmail: null,
dockerVolumeCleaner: null,
dynamicDns: null,
schedulerSync: null,
appHealthMonitor: null
dynamicDNS: null,
schedulerSync: null
};
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
@@ -59,7 +55,9 @@ var AUDIT_SOURCE = { userId: null, username: 'cron' };
// Months: 0-11
// Day of Week: 0-6
function startPreActivationJobs(callback) {
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (config.provider() === 'caas') {
// hack: send the first heartbeat only after we are running for 60 seconds
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
@@ -73,12 +71,6 @@ function startPreActivationJobs(callback) {
});
}
callback();
}
function startPostActivationJobs(callback) {
assert.strictEqual(typeof callback, 'function');
var randomHourMinute = Math.floor(60*Math.random());
gJobs.alive = new CronJob({
cronTime: '00 ' + randomHourMinute + ' * * * *', // every hour on a random minute
@@ -86,10 +78,10 @@ function startPostActivationJobs(callback) {
start: true
});
settings.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
@@ -110,7 +102,7 @@ function recreateJobs(tz) {
if (gJobs.backup) gJobs.backup.stop();
gJobs.backup = new CronJob({
cronTime: '00 00 */6 * * *', // check every 6 hours
cronTime: '00 00 */6 * * *', // every 6 hours. backups.ensureBackup() will only trigger a backup once per day
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
@@ -198,14 +190,6 @@ function recreateJobs(tz) {
start: true,
timeZone: tz
});
if (gJobs.appHealthMonitor) gJobs.appHealthMonitor.stop();
gJobs.appHealthMonitor = new CronJob({
cronTime: '*/10 * * * * *', // every 10 seconds
onTick: appHealthMonitor.run.bind(null, 10),
start: true,
timeZone: tz
});
}
function boxAutoupdatePatternChanged(pattern) {
@@ -267,25 +251,25 @@ function dynamicDnsChanged(enabled) {
debug('Dynamic DNS setting changed to %s', enabled);
if (enabled) {
gJobs.dynamicDns = new CronJob({
gJobs.dynamicDNS = new CronJob({
cronTime: '00 */10 * * * *',
onTick: dyndns.sync,
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
} else {
if (gJobs.dynamicDns) gJobs.dynamicDns.stop();
gJobs.dynamicDns = null;
if (gJobs.dynamicDNS) gJobs.dynamicDNS.stop();
gJobs.dynamicDNS = null;
}
}
function stopJobs(callback) {
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
settings.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
for (var job in gJobs) {
if (!gJobs[job]) continue;
+1 -2
View File
@@ -130,8 +130,7 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
var credentials = {
token: dnsConfig.token,
fqdn: domain,
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
fqdn: domain
};
const testSubdomain = 'cloudrontestdns';
+7 -2
View File
@@ -28,7 +28,12 @@ function translateRequestError(result, callback) {
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
let error = result.body.errors[0];
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
let message = error.message;
if (error.code === 6003) {
if (error.error_chain[0] && error.error_chain[0].code === 6103) message = 'Invalid API Key';
else message = 'Invalid credentials';
}
return callback(new DomainsError(DomainsError.ACCESS_DENIED, message));
}
@@ -107,7 +112,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
var priority = null;
if (type === 'MX') {
priority = parseInt(value.split(' ')[0], 10);
priority = value.split(' ')[0];
value = value.split(' ')[1];
}
+2 -3
View File
@@ -21,7 +21,6 @@ const GODADDY_API = 'https://api.godaddy.com/v1/domains';
// this is a workaround for godaddy not having a delete API
// https://stackoverflow.com/questions/39347464/delete-record-libcloud-godaddy-api
const GODADDY_INVALID_IP = '0.0.0.0';
const GODADDY_INVALID_TXT = '""';
function formatError(response) {
return util.format(`GoDaddy DNS error [${response.statusCode}] ${response.body.message}`);
@@ -110,7 +109,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
debug(`get: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
if (type !== 'A' && type !== 'TXT') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Record deletion is not supported by GoDaddy API')));
if (type !== 'A') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Not supported by GoDaddy API'))); // can never happen
// check if the record exists at all so that we don't insert the "Dead" record for no reason
get(dnsConfig, zoneName, subdomain, type, function (error, values) {
@@ -120,7 +119,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
// godaddy does not have a delete API. so fill it up with an invalid IP that we can ignore in future get()
var records = [{
ttl: 600,
data: type === 'A' ? GODADDY_INVALID_IP : GODADDY_INVALID_TXT
data: GODADDY_INVALID_IP
}];
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
+1 -1
View File
@@ -60,6 +60,6 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
callback(null, {});
callback(null, { wildcard: !!dnsConfig.wildcard });
});
}
-3
View File
@@ -208,9 +208,6 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
if (typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a string'));
if (typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a string'));
var credentials = {
username: dnsConfig.username,
token: dnsConfig.token
+1 -2
View File
@@ -46,10 +46,9 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function waitForDns(domain, zoneName, type, value, options, callback) {
function waitForDns(domain, zoneName, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
+2 -2
View File
@@ -114,7 +114,7 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
};
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error) {
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'AccessDenied') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 'PriorRequestNotComplete') return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
@@ -241,7 +241,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
secretAccessKey: dnsConfig.secretAccessKey,
region: dnsConfig.region || 'us-east-1',
endpoint: dnsConfig.endpoint || null,
listHostedZonesByName: true, // new/updated creds require this perm
listHostedZonesByName: true // new/updated creds require this perm
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
+8 -20
View File
@@ -30,9 +30,8 @@ function resolveIp(hostname, options, callback) {
});
}
function isChangeSynced(domain, type, value, nameserver, callback) {
function isChangeSynced(domain, value, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof nameserver, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -45,30 +44,20 @@ function isChangeSynced(domain, type, value, nameserver, callback) {
}
async.every(nsIps, function (nsIp, iteratorCallback) {
const resolveOptions = { server: nsIp, timeout: 5000 };
const resolver = type === 'A' ? resolveIp.bind(null, domain) : dns.resolve.bind(null, domain, 'TXT');
resolver(resolveOptions, function (error, answer) {
resolveIp(domain, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'TIMEOUT') {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain} (${type})`);
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain}`);
return iteratorCallback(null, true); // should be ok if dns server is down
}
if (error) {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain} (${type}): ${error}`);
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain}: ${error}`);
return iteratorCallback(null, false);
}
let match;
if (type === 'A') {
match = answer.length === 1 && answer[0] === value;
} else if (type === 'TXT') { // answer is a 2d array of strings
match = answer.some(function (a) { return value === a.join(''); });
}
debug(`isChangeSynced: ${domain} was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}`);
debug(`isChangeSynced: ${domain} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`);
iteratorCallback(null, match);
iteratorCallback(null, answer.length === 1 && answer[0] === value);
});
}, callback);
@@ -76,10 +65,9 @@ function isChangeSynced(domain, type, value, nameserver, callback) {
}
// check if IP change has propagated to every nameserver
function waitForDns(domain, zoneName, type, value, options, callback) {
function waitForDns(domain, zoneName, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert(type === 'A' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
@@ -94,7 +82,7 @@ function waitForDns(domain, zoneName, type, value, options, callback) {
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new DomainsError(DomainsError.EXTERNAL_ERROR, 'Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, domain, type, value), function (error, synced) {
async.every(nameservers, isChangeSynced.bind(null, domain, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
retryCallback(synced ? null : new DomainsError(DomainsError.EXTERNAL_ERROR, 'ETRYAGAIN'));
-79
View File
@@ -1,79 +0,0 @@
'use strict';
exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError,
sysinfo = require('../sysinfo.js'),
util = require('util');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
return callback(null);
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
callback(null, [ ]); // returning ip confuses apptask into thinking the entry already exists
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
return callback();
}
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
const separator = dnsConfig.hyphenatedSubdomains ? '-' : '.';
const fqdn = `cloudrontest${separator}${domain}`;
dns.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 }, function (error, result) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, `Unable to resolve ${fqdn}`));
if (error || !result) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : `Unable to resolve ${fqdn}`));
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Failed to detect IP of this server: ${error.message}`));
if (result.length !== 1 || ip !== result[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(result)} instead of ${ip}`));
callback(null, {});
});
});
});
}
+11 -131
View File
@@ -2,8 +2,6 @@
exports = module.exports = {
connection: connectionInstance(),
setRegistryConfig: setRegistryConfig,
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
@@ -18,10 +16,7 @@ exports = module.exports = {
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
execContainer: execContainer,
createVolume: createVolume,
removeVolume: removeVolume,
clearVolume: clearVolume
execContainer: execContainer
};
function connectionInstance() {
@@ -48,62 +43,19 @@ var addons = require('./addons.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:docker.js'),
mkdirp = require('mkdirp'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
spawn = child_process.spawn,
util = require('util'),
_ = require('underscore');
const RMVOLUME_CMD = path.join(__dirname, 'scripts/rmvolume.sh');
function DockerError(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(DockerError, Error);
DockerError.INTERNAL_ERROR = 'Internal Error';
DockerError.BAD_FIELD = 'Bad field';
function debugApp(app, args) {
assert(typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function setRegistryConfig(auth, callback) {
assert.strictEqual(typeof auth, 'object');
assert.strictEqual(typeof callback, 'function');
const isLogin = !!auth.password;
// currently, auth info is not stashed in the db but maybe it should for restore to work?
const cmd = isLogin ? `docker login ${auth.serveraddress} --username ${auth.username} --password ${auth.password}` : `docker logout ${auth.serveraddress}`;
child_process.exec(cmd, { }, function (error, stdout, stderr) {
if (error) return callback(new DockerError(DockerError.BAD_FIELD, stderr));
callback();
});
}
function pullImage(manifest, callback) {
var docker = exports.connection;
@@ -175,22 +127,16 @@ function createSubcontainer(app, name, cmd, options, callback) {
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
var portEnv = [];
for (let portName in app.portBindings) {
const hostPort = app.portBindings[portName];
const portType = portName in manifest.tcpPorts ? 'tcp' : 'udp';
const ports = portType == 'tcp' ? manifest.tcpPorts : manifest.udpPorts;
for (var e in app.portBindings) {
var hostPort = app.portBindings[e];
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
var containerPort = ports[portName].containerPort || hostPort;
exposedPorts[containerPort + '/tcp'] = {};
portEnv.push(e + '=' + hostPort);
exposedPorts[`${containerPort}/${portType}`] = {};
portEnv.push(`${portName}=${hostPort}`);
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
}
let appEnv = [];
Object.keys(app.env).forEach(function (name) { appEnv.push(`${name}=${app.env[name]}`); });
// first check db record, then manifest
var memoryLimit = app.memoryLimit || manifest.memoryLimit || 0;
@@ -218,10 +164,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
var containerOptions = {
name: name, // used for filtering logs
Tty: isAppContainer,
Hostname: app.id, // set to something 'constant' so app containers can use this to communicate (across app updates)
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
Env: stdEnv.concat(addonEnv).concat(portEnv),
ExposedPorts: isAppContainer ? exposedPorts : { },
Volumes: { // see also ReadonlyRootfs
'/tmp': {},
@@ -230,11 +175,10 @@ function createSubcontainer(app, name, cmd, options, callback) {
Labels: {
'fqdn': app.fqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer),
'isCloudronManaged': String(true)
'isSubcontainer': String(!isAppContainer)
},
HostConfig: {
Mounts: addons.getMountsSync(app, app.manifest.addons),
Binds: addons.getBindsSync(app, app.manifest.addons),
LogConfig: {
Type: 'syslog',
Config: {
@@ -405,8 +349,7 @@ function deleteImage(manifest, callback) {
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
// not created anymore after https://github.com/docker/docker/pull/10571
docker.getImage(dockerImage).remove(removeOptions, function (error) {
if (error && error.statusCode === 400) return callback(null); // invalid image format. this can happen if user installed with a bad --docker-image
if (error && error.statusCode === 404) return callback(null); // not found
if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image
if (error) debug('Error removing image %s : %j', dockerImage, error);
@@ -480,66 +423,3 @@ function execContainer(containerId, cmd, options, callback) {
if (options.stdin) options.stdin.pipe(cp.stdin).on('error', callback);
}
function createVolume(app, name, subdir, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdir, 'string');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
const volumeDataDir = path.join(paths.APPS_DATA_DIR, app.id, subdir);
const volumeOptions = {
Name: name,
Driver: 'local',
DriverOpts: { // https://github.com/moby/moby/issues/19990#issuecomment-248955005
type: 'none',
device: volumeDataDir,
o: 'bind'
},
Labels: {
'fqdn': app.fqdn,
'appId': app.id
},
};
mkdirp(volumeDataDir, function (error) {
if (error) return callback(new Error(`Error creating app data dir: ${error.message}`));
docker.createVolume(volumeOptions, function (error) {
if (error) return callback(error);
callback();
});
});
}
function clearVolume(app, name, subdir, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdir, 'string');
assert.strictEqual(typeof callback, 'function');
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
}
function removeVolume(app, name, subdir, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdir, 'string');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
let volume = docker.getVolume(name);
volume.remove(function (error) {
if (error && error.statusCode !== 404) {
debug(`removeVolume: Error removing volume of ${app.id} ${error}`);
callback(error);
}
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
});
}
-178
View File
@@ -1,178 +0,0 @@
'use strict';
exports = module.exports = {
start: start,
stop: stop
};
var apps = require('./apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
config = require('./config.js'),
express = require('express'),
debug = require('debug')('box:dockerproxy'),
http = require('http'),
HttpError = require('connect-lastmile').HttpError,
middleware = require('./middleware'),
net = require('net'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
_ = require('underscore');
var gHttpServer = null;
function authorizeApp(req, res, next) {
// TODO add here some authorization
// - block apps not using the docker addon
// - block calls regarding platform containers
// - only allow managing and inspection of containers belonging to the app
// make the tests pass for now
if (config.TEST) {
req.app = { id: 'testappid' };
return next();
}
apps.getByIpAddress(req.connection.remoteAddress, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
if (error) return next(new HttpError(500, error));
if (!('docker' in app.manifest.addons)) return next(new HttpError(401, 'Unauthorized'));
req.app = app;
next();
});
}
function attachDockerRequest(req, res, next) {
var options = {
socketPath: '/var/run/docker.sock',
method: req.method,
path: req.url,
headers: req.headers
};
req.dockerRequest = http.request(options, function (dockerResponse) {
res.writeHead(dockerResponse.statusCode, dockerResponse.headers);
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
res.write(' ');
dockerResponse.on('error', function (error) { console.error('dockerResponse error:', error); });
dockerResponse.pipe(res, { end: true });
});
next();
}
function containersCreate(req, res, next) {
safe.set(req.body, 'HostConfig.NetworkMode', 'cloudron'); // overwrite the network the container lives in
safe.set(req.body, 'NetworkingConfig', {}); // drop any custom network configs
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id })); // overwrite the app id to track containers of an app
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
dockerDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'docker');
debug('Original volume binds:', req.body.HostConfig.Binds);
let binds = [];
for (let bind of (req.body.HostConfig.Binds || [])) {
if (bind.startsWith(appDataDir)) binds.push(bind); // eclipse will inspect docker to find out the host folders and pass that to child containers
else if (bind.startsWith('/app/data')) binds.push(bind.replace(new RegExp('^/app/data'), appDataDir));
else binds.push(`${dockerDataDir}/${bind}`);
}
// cleanup the paths from potential double slashes
binds = binds.map(function (bind) { return bind.replace(/\/+/g, '/'); });
debug('Rewritten volume binds:', binds);
safe.set(req.body, 'HostConfig.Binds', binds);
let plainBody = JSON.stringify(req.body);
req.dockerRequest.setHeader('Content-Length', Buffer.byteLength(plainBody));
req.dockerRequest.end(plainBody);
}
function process(req, res, next) {
// we have to rebuild the body since we consumed in in the parser
if (Object.keys(req.body).length !== 0) {
let plainBody = JSON.stringify(req.body);
req.dockerRequest.setHeader('Content-Length', Buffer.byteLength(plainBody));
req.dockerRequest.end(plainBody);
} else if (!req.readable) {
req.dockerRequest.end();
} else {
req.pipe(req.dockerRequest, { end: true });
}
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
assert(gHttpServer === null, 'Already started');
let json = middleware.json({ strict: true });
let router = new express.Router();
router.post('/:version/containers/create', containersCreate);
let proxyServer = express();
if (config.TEST) {
proxyServer.use(function (req, res, next) {
console.log('Proxying: ' + req.method, req.url);
next();
});
}
proxyServer.use(authorizeApp)
.use(attachDockerRequest)
.use(json)
.use(router)
.use(process)
.use(middleware.lastMile());
gHttpServer = http.createServer(proxyServer);
gHttpServer.listen(config.get('dockerProxyPort'), '0.0.0.0', callback);
debug(`startDockerProxy: started proxy on port ${config.get('dockerProxyPort')}`);
gHttpServer.on('upgrade', function (req, client, head) {
// Create a new tcp connection to the TCP server
var remote = net.connect('/var/run/docker.sock', function () {
var upgradeMessage = req.method + ' ' + req.url + ' HTTP/1.1\r\n' +
`Host: ${req.headers.host}\r\n` +
'Connection: Upgrade\r\n' +
'Upgrade: tcp\r\n';
if (req.headers['content-type'] === 'application/json') {
// TODO we have to parse the immediate upgrade request body, but I don't know how
let plainBody = '{"Detach":false,"Tty":false}\r\n';
upgradeMessage += `Content-Type: application/json\r\n`;
upgradeMessage += `Content-Length: ${Buffer.byteLength(plainBody)}\r\n`;
upgradeMessage += '\r\n';
upgradeMessage += plainBody;
}
upgradeMessage += '\r\n';
// resend the upgrade event to the docker daemon, so it responds with the proper message through the pipes
remote.write(upgradeMessage);
// two-way pipes between client and docker daemon
client.pipe(remote).pipe(client);
});
});
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
if (gHttpServer) gHttpServer.close();
gHttpServer = null;
callback();
}
+66 -211
View File
@@ -6,9 +6,6 @@ module.exports = exports = {
getAll: getAll,
update: update,
del: del,
isLocked: isLocked,
renewCerts: renewCerts,
fqdn: fqdn,
setAdmin: setAdmin,
@@ -22,26 +19,15 @@ module.exports = exports = {
removePrivateFields: removePrivateFields,
removeRestrictedFields: removeRestrictedFields,
validateHostname: validateHostname,
makeWildcard: makeWildcard,
parentDomain: parentDomain,
DomainsError: DomainsError,
// exported for testing
_getName: getName
DomainsError: DomainsError
};
var assert = require('assert'),
caas = require('./caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
eventlog = require('./eventlog.js'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
@@ -83,7 +69,7 @@ DomainsError.STILL_BUSY = 'Still busy';
DomainsError.IN_USE = 'In Use';
DomainsError.INTERNAL_ERROR = 'Internal error';
DomainsError.ACCESS_DENIED = 'Access denied';
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, wildcard, manual or caas';
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, manual or caas';
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
@@ -100,18 +86,12 @@ function api(provider) {
case 'namecom': return require('./dns/namecom.js');
case 'noop': return require('./dns/noop.js');
case 'manual': return require('./dns/manual.js');
case 'wildcard': return require('./dns/wildcard.js');
default: return null;
}
}
function parentDomain(domain) {
assert.strictEqual(typeof domain, 'string');
return domain.replace(/^\S+?\./, ''); // +? means non-greedy
}
function verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, callback) {
assert(dnsConfig && typeof dnsConfig === 'object'); // the dns config to test with
function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
assert(config && typeof config === 'object'); // the dns config to test with
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
@@ -119,98 +99,21 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, callback) {
assert.strictEqual(typeof callback, 'function');
var backend = api(provider);
if (!backend) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid provider'));
if (!backend) return callback(new DomainsError(DomainsError.INVALID_PROVIDER));
api(provider).verifyDnsConfig(dnsConfig, domain, zoneName, ip, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Incorrect configuration. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Configuration error: ' + error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
result.hyphenatedSubdomains = !!dnsConfig.hyphenatedSubdomains;
callback(null, result);
});
api(provider).verifyDnsConfig(config, domain, zoneName, ip, callback);
}
function fqdn(location, domainObject) {
return location + (location ? (domainObject.config.hyphenatedSubdomains ? '-' : '.') : '') + domainObject.domain;
}
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name (and not dns name)
function validateHostname(location, domainObject) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domainObject, 'object');
const hostname = fqdn(location, domainObject);
const RESERVED_LOCATIONS = [
constants.API_LOCATION,
constants.SMTP_LOCATION,
constants.IMAP_LOCATION
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
if (hostname === config.adminFqdn()) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
// workaround https://github.com/oncletom/tld.js/issues/73
var tmp = hostname.replace('_', '-');
if (!tld.isValid(tmp)) return new DomainsError(DomainsError.BAD_FIELD, 'Hostname is not a valid domain name');
if (hostname.length > 253) return new DomainsError(DomainsError.BAD_FIELD, 'Hostname length exceeds 253 characters');
if (location) {
// label validation
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new DomainsError(DomainsError.BAD_FIELD, 'Invalid subdomain length');
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot');
if (/^[-.]/.test(location)) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot');
}
if (domainObject.config.hyphenatedSubdomains) {
if (location.indexOf('.') !== -1) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain cannot contain a dot');
}
return null;
}
function validateTlsConfig(tlsConfig, dnsProvider) {
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof dnsProvider, 'string');
switch (tlsConfig.provider) {
case 'letsencrypt-prod':
case 'letsencrypt-staging':
case 'fallback':
case 'caas':
break;
default:
return new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback, letsencrypt-prod/staging');
}
if (tlsConfig.wildcard) {
if (!tlsConfig.provider.startsWith('letsencrypt')) return new DomainsError(DomainsError.BAD_FIELD, 'wildcard can only be set with letsencrypt');
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') return new DomainsError(DomainsError.BAD_FIELD, 'wildcard cert requires a programmable DNS backend');
}
return null;
}
function add(domain, data, auditSource, callback) {
function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data.zoneName, 'string');
assert.strictEqual(typeof data.provider, 'string');
assert.strictEqual(typeof data.config, 'object');
assert.strictEqual(typeof data.fallbackCertificate, 'object');
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
if (!tld.isValid(domain)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
if (domain.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
@@ -222,31 +125,32 @@ function add(domain, data, auditSource, callback) {
}
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate('test', { domain, config }, fallbackCertificate);
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
} else {
fallbackCertificate = reverseProxy.generateFallbackCertificateSync({ domain, config });
if (fallbackCertificate.error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, fallbackCertificate.error));
}
let error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or le-*'));
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, sanitizedConfig) {
if (error) return callback(error);
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record: ' + error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainsError(DomainsError.ALREADY_EXISTS));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
callback();
});
});
@@ -254,10 +158,6 @@ function add(domain, data, auditSource, callback) {
});
}
function isLocked(domain) {
return domain === config.adminDomain() && config.edition() === 'hostingprovider';
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -267,8 +167,6 @@ function get(domain, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
result.locked = isLocked(domain);
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
@@ -290,49 +188,50 @@ function getAll(callback) {
domaindb.getAll(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
result.forEach(function (r) { r.locked = isLocked(r.domain); });
return callback(null, result);
});
}
function update(domain, data, auditSource, callback) {
function update(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data.zoneName, 'string');
assert.strictEqual(typeof data.provider, 'string');
assert.strictEqual(typeof data.config, 'object');
assert.strictEqual(typeof data.fallbackCertificate, 'object');
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
domaindb.get(domain, function (error, domainObject) {
domaindb.get(domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (zoneName) {
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
} else {
zoneName = domainObject.zoneName;
zoneName = result.zoneName;
}
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate('test', domainObject, fallbackCertificate);
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
}
error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or letsencrypt-*'));
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, sanitizedConfig) {
if (error) return callback(error);
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record:' + error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
@@ -341,8 +240,6 @@ function update(domain, data, auditSource, callback) {
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
callback();
});
});
@@ -351,46 +248,28 @@ function update(domain, data, auditSource, callback) {
});
}
function del(domain, auditSource, callback) {
function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (domain === config.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
domaindb.del(domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error && error.reason === DatabaseError.IN_USE) return callback(new DomainsError(DomainsError.IN_USE));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
return callback(null);
});
}
// returns the 'name' that needs to be inserted into zone
function getName(domain, subdomain, type) {
// hack for supporting special caas domains. if we want to remove this, we have to fix the appstore domain API first
function getName(domain, subdomain) {
// support special caas domains
if (domain.provider === 'caas') return subdomain;
const part = domain.domain.slice(0, -domain.zoneName.length - 1);
if (domain.domain === domain.zoneName) return subdomain;
if (subdomain === '') return part;
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
if (!domain.config.hyphenatedSubdomains) return part ? `${subdomain}.${part}` : subdomain;
// hyphenatedSubdomains
if (type !== 'TXT') return `${subdomain}-${part}`;
if (subdomain.startsWith('_acme-challenge.')) {
return `${subdomain}-${part}`;
} else if (subdomain === '_acme-challenge') {
const up = part.replace(/^[^.]*\.?/, ''); // this gets the domain one level up
return up ? `${subdomain}.${up}` : subdomain;
} else {
return `${subdomain}.${part}`;
}
return subdomain === '' ? part : subdomain + '.' + part;
}
function getDnsRecords(subdomain, domain, type, callback) {
@@ -402,7 +281,7 @@ function getDnsRecords(subdomain, domain, type, callback) {
get(domain, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain, type), type, function (error, values) {
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain), type, function (error, values) {
if (error) return callback(error);
callback(null, values);
@@ -410,7 +289,6 @@ function getDnsRecords(subdomain, domain, type, callback) {
});
}
// note: for TXT records the values must be quoted
function upsertDnsRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
@@ -423,7 +301,7 @@ function upsertDnsRecords(subdomain, domain, type, values, callback) {
get(domain, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
if (error) return callback(error);
callback(null);
@@ -443,7 +321,7 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
get(domain, function (error, result) {
if (error) return callback(error);
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(error);
callback(null);
@@ -451,20 +329,18 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
});
}
function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
assert.strictEqual(typeof subdomain, 'string');
// only wait for A record
function waitForDnsRecord(fqdn, domain, value, options, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert(type === 'A' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, domainObject) {
get(domain, function (error, result) {
if (error) return callback(error);
const hostname = fqdn(subdomain, domainObject);
api(domainObject.provider).waitForDns(hostname, domainObject.zoneName, type, value, options, callback);
api(result.provider).waitForDns(fqdn, result ? result.zoneName : domain, value, options, callback);
});
}
@@ -484,7 +360,7 @@ function setAdmin(domain, callback) {
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
config.setAdminFqdn('my' + (result.provider === 'caas' ? '-' : '.') + result.domain);
callback();
@@ -493,40 +369,19 @@ function setAdmin(domain, callback) {
});
}
function fqdn(location, domain, provider) {
return location + (location ? (provider !== 'caas' ? '.' : '-') : '') + domain;
}
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'locked');
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate');
if (result.fallbackCertificate) delete result.fallbackCertificate.key; // do not return the 'key'. in caas, this is private
return result;
}
// removes all fields that are not accessible by a normal user
function removeRestrictedFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'locked');
// always ensure config object
result.config = { hyphenatedSubdomains: !!domain.config.hyphenatedSubdomains };
var result = _.pick(domain, 'domain', 'zoneName', 'provider');
return result;
}
function makeWildcard(hostname) {
assert.strictEqual(typeof hostname, 'string');
let parts = hostname.split('.');
parts[0] = '*';
return parts.join('.');
}
function renewCerts(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
// trigger renewal in the background
reverseProxy.renewCerts({ domain: domain }, auditSource, function (error) {
debug('renewCerts', error);
});
callback();
}
}
-18
View File
@@ -18,30 +18,12 @@ exports = module.exports = {
ACTION_APP_UNINSTALL: 'app.uninstall',
ACTION_APP_UPDATE: 'app.update',
ACTION_APP_LOGIN: 'app.login',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
ACTION_CERTIFICATE_NEW: 'certificate.new',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
ACTION_DOMAIN_ADD: 'domain.add',
ACTION_DOMAIN_UPDATE: 'domain.update',
ACTION_DOMAIN_REMOVE: 'domain.remove',
ACTION_MAIL_ENABLED: 'mail.enabled',
ACTION_MAIL_DISABLED: 'mail.disabled',
ACTION_MAIL_MAILBOX_ADD: 'mail.box.add',
ACTION_MAIL_MAILBOX_REMOVE: 'mail.box.remove',
ACTION_MAIL_LIST_ADD: 'mail.list.add',
ACTION_MAIL_LIST_REMOVE: 'mail.list.remove',
ACTION_PROVISION: 'cloudron.provision',
ACTION_RESTORE: 'cloudron.restore', // unused
ACTION_START: 'cloudron.start',
ACTION_UPDATE: 'cloudron.update',
ACTION_USER_ADD: 'user.add',
ACTION_USER_LOGIN: 'user.login',
ACTION_USER_REMOVE: 'user.remove',
-42
View File
@@ -1,42 +0,0 @@
'use strict';
exports = module.exports = {
startGraphite: startGraphite
};
var assert = require('assert'),
infra = require('./infra_version.js'),
paths = require('./paths.js'),
shell = require('./shell.js');
function startGraphite(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
const tag = infra.images.graphite.tag;
const dataDir = paths.PLATFORM_DATA_DIR;
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
const cmd = `docker run --restart=always -d --name="graphite" \
--net cloudron \
--net-alias graphite \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=graphite \
-m 75m \
--memory-swap 150m \
--dns 172.18.0.1 \
--dns-search=. \
-p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8000:8000 \
-v "${dataDir}/graphite:/var/lib/graphite" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startGraphite', cmd);
callback();
}
+12 -13
View File
@@ -5,21 +5,20 @@
// Do not require anything here!
exports = module.exports = {
// a version change recreates all containers with latest docker config
'version': '48.12.1',
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
// a minor version makes all apps re-configure themselves
'version': '48.11.0',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
],
'baseImages': [ 'cloudron/base:0.10.0' ],
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
// Note that if any of the databases include an upgrade, bump the infra version above
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.0.1@sha256:5a13360da4a2085c7d474bea6b1090c5eb24732d4f73459942af7612d4993d7f' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.0.0@sha256:3c0fbb2a042ac471940ac3e9f6ffa900c8a294941fb7de509b2e3309b09fbffd' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.0@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.1.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.1.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.1.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.4.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
}
};
+38 -30
View File
@@ -10,6 +10,7 @@ var assert = require('assert'),
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:ldap'),
eventlog = require('./eventlog.js'),
@@ -28,32 +29,37 @@ var NOOP = function () {};
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
// Will attach req.app if successful
function authenticateApp(req, res, next) {
function getAppByRequest(req, callback) {
assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof callback, 'function');
var sourceIp = req.connection.ldap.id.split(':')[0];
if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier'));
if (sourceIp.split('.').length !== 4) return callback(new ldap.InsufficientAccessRightsError('Missing source identifier'));
apps.getByIpAddress(sourceIp, function (error, app) {
if (error) return next(new ldap.OperationsError(error.message));
if (!app) return next(new ldap.OperationsError('Could not detect app source'));
if (error) return callback(new ldap.OperationsError(error.message));
req.app = app;
if (!app) return callback(new ldap.OperationsError('Could not detect app source'));
next();
callback(null, app);
});
}
function getUsersWithAccessToApp(req, callback) {
assert.strictEqual(typeof req.app, 'object');
assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof callback, 'function');
users.list(function (error, result) {
if (error) return callback(new ldap.OperationsError(error.toString()));
getAppByRequest(req, function (error, app) {
if (error) return callback(error);
async.filter(result, apps.hasAccessTo.bind(null, req.app), function (error, allowedUsers) {
users.list(function (error, result) {
if (error) return callback(new ldap.OperationsError(error.toString()));
callback(null, allowedUsers);
async.filter(result, apps.hasAccessTo.bind(null, app), function (error, allowedUsers) {
if (error) return callback(new ldap.OperationsError(error.toString()));
callback(null, allowedUsers);
});
});
});
}
@@ -134,7 +140,7 @@ function userSearch(req, res, next) {
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
var groups = [ GROUP_USERS_DN ];
if (entry.admin || req.app.ownerId === entry.id) groups.push(GROUP_ADMINS_DN);
if (entry.admin) groups.push(GROUP_ADMINS_DN);
var displayName = entry.displayName || entry.username || ''; // displayName can be empty and username can be null
var nameParts = displayName.split(' ');
@@ -154,7 +160,7 @@ function userSearch(req, res, next) {
givenName: firstName,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
isadmin: (entry.admin || req.app.ownerId === entry.id) ? 1 : 0,
isadmin: entry.admin ? 1 : 0,
memberof: groups
}
};
@@ -194,7 +200,7 @@ function groupSearch(req, res, next) {
groups.forEach(function (group) {
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
var members = group.admin ? result.filter(function (entry) { return entry.admin || req.app.ownerId === entry.id; }) : result;
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
var obj = {
dn: dn.toString(),
@@ -243,7 +249,7 @@ function groupAdminsCompare(req, res, next) {
// we only support memberuid here, if we add new group attributes later add them here
if (req.attribute === 'memberuid') {
var found = result.find(function (u) { return u.id === req.value; });
if (found && (found.admin || req.app.ownerId == found.id)) return res.end(true);
if (found && found.admin) return res.end(true);
}
res.end(false);
@@ -408,7 +414,6 @@ function mailingListSearch(req, res, next) {
});
}
// Will attach req.user if successful
function authenticateUser(req, res, next) {
debug('user bind: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
@@ -440,18 +445,21 @@ function authenticateUser(req, res, next) {
}
function authorizeUserForApp(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.app, 'object');
assert(req.user);
apps.hasAccessTo(req.app, req.user, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
getAppByRequest(req, function (error, app) {
if (error) return next(error);
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
apps.hasAccessTo(app, req.user, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id, app: req.app }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
res.end();
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id, app: app }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
res.end();
});
});
}
@@ -518,9 +526,9 @@ function start(callback) {
gServer = ldap.createServer({ log: logger });
gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch);
gServer.search('ou=groups,dc=cloudron', authenticateApp, groupSearch);
gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp);
gServer.search('ou=users,dc=cloudron', userSearch);
gServer.search('ou=groups,dc=cloudron', groupSearch);
gServer.bind('ou=users,dc=cloudron', authenticateUser, authorizeUserForApp);
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch);
@@ -531,8 +539,8 @@ function start(callback) {
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
gServer.compare('cn=users,ou=groups,dc=cloudron', groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', groupAdminsCompare);
// this is the bind for addons (after bind, they might search and authenticate)
gServer.bind('ou=addons,dc=cloudron', function(req, res /*, next */) {
+16 -64
View File
@@ -51,7 +51,6 @@ var assert = require('assert'),
debug = require('debug')('box:mail'),
dns = require('./native-dns.js'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
infra = require('./infra_version.js'),
mailboxdb = require('./mailboxdb.js'),
maildb = require('./maildb.js'),
@@ -279,15 +278,6 @@ function checkMx(domain, callback) {
});
}
function txtToDict(txt) {
var dict = {};
txt.split(';').forEach(function(v) {
var p = v.trim().split('=');
dict[p[0]]=p[1];
});
return dict;
}
function checkDmarc(domain, callback) {
var dmarc = {
domain: '_dmarc.' + domain,
@@ -303,9 +293,7 @@ function checkDmarc(domain, callback) {
if (txtRecords.length !== 0) {
dmarc.value = txtRecords[0].join('');
// allow extra fields in dmarc like rua
const actual = txtToDict(dmarc.value), expected = txtToDict(dmarc.expected);
dmarc.status = Object.keys(expected).every(k => expected[k] === actual[k]);
dmarc.status = (dmarc.value === dmarc.expected);
}
callback(null, dmarc);
@@ -554,7 +542,7 @@ function restartMail(callback) {
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
const tag = infra.images.mail.tag;
const memoryLimit = 4 * 256;
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
// admin and mail share the same certificate
reverseProxy.getCertificate({ fqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
@@ -589,7 +577,6 @@ function restartMail(callback) {
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
-p 127.0.0.1:2020:2020 \
--label isCloudronManaged=true \
--read-only -v /run -v /tmp ${tag}`;
shell.execSync('startMail', cmd);
@@ -599,20 +586,6 @@ function restartMail(callback) {
});
}
function restartMailIfActivated(callback) {
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (!activated) {
debug('restartMailIfActivated: skipping restart of mail container since Cloudron is not activated yet');
return callback(); // not provisioned yet, do not restart container after dns setup
}
restartMail(callback);
});
}
function getDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -641,7 +614,7 @@ function txtRecordsWithSpf(domain, callback) {
assert.strictEqual(typeof callback, 'function');
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
if (error) return new MailError(MailError.EXTERNAL_ERROR, error.message);
if (error) return callback(error);
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
@@ -745,26 +718,22 @@ function setDnsRecords(domain, callback) {
records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] });
}
debug('setDnsRecords: %j', records);
debug('addDnsRecords: %j', records);
txtRecordsWithSpf(domain, function (error, txtRecords) {
if (error) return callback(error);
if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords });
debug('setDnsRecords: will update %j', records);
debug('addDnsRecords: will update %j', records);
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) {
debug(`setDnsRecords: failed to update: ${error}`);
return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
}
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
debug('setDnsRecords: records %j added with changeIds %j', records, changeIds);
callback(null);
callback(error);
});
});
});
@@ -781,7 +750,7 @@ function addDomain(domain, callback) {
async.series([
setDnsRecords.bind(null, domain), // do this first to ensure DKIM keys
restartMailIfActivated
restartMail
], NOOP_CALLBACK); // do these asynchronously
callback();
@@ -792,8 +761,6 @@ function removeDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (domain === config.adminDomain()) return callback(new MailError(MailError.IN_USE));
maildb.del(domain, function (error) {
if (error && error.reason === DatabaseError.IN_USE) return callback(new MailError(MailError.IN_USE));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, error.message));
@@ -854,10 +821,9 @@ function setMailRelay(domain, relay, callback) {
});
}
function setMailEnabled(domain, enabled, auditSource, callback) {
function setMailEnabled(domain, enabled, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
maildb.update(domain, { enabled: enabled }, function (error) {
@@ -866,8 +832,6 @@ function setMailEnabled(domain, enabled, auditSource, callback) {
restartMail(NOOP_CALLBACK);
eventlog.add(enabled ? eventlog.ACTION_MAIL_ENABLED : eventlog.ACTION_MAIL_DISABLED, auditSource, { domain });
callback(null);
});
}
@@ -921,11 +885,10 @@ function getMailbox(name, domain, callback) {
});
}
function addMailbox(name, domain, userId, auditSource, callback) {
function addMailbox(name, domain, userId, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
name = name.toLowerCase();
@@ -937,8 +900,6 @@ function addMailbox(name, domain, userId, auditSource, callback) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, `mailbox ${name} already exists`));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, userId });
callback(null);
});
}
@@ -962,18 +923,15 @@ function updateMailbox(name, domain, userId, callback) {
});
}
function removeMailbox(name, domain, auditSource, callback) {
function removeMailbox(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
mailboxdb.del(name, domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
callback(null);
});
}
@@ -1058,11 +1016,10 @@ function getList(domain, listName, callback) {
});
}
function addList(name, domain, members, auditSource, callback) {
function addList(name, domain, members, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
name = name.toLowerCase();
@@ -1081,8 +1038,6 @@ function addList(name, domain, members, auditSource, callback) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'list already exits'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain });
callback();
});
}
@@ -1113,18 +1068,15 @@ function updateList(name, domain, members, callback) {
});
}
function removeList(name, domain, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
function removeList(domain, listName, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.del(name, domain, function (error) {
mailboxdb.del(listName, domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such list'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain });
callback();
});
}
+17
View File
@@ -4,6 +4,15 @@ Dear <%= cloudronName %> Admin,
A new user with email <%= user.email %> was added to <%= cloudronName %>.
<% if (inviteLink) { %>
As requested, this user has not been sent an invitation email.
To set a password and perform any configuration on behalf of the user, please use this link:
<%= inviteLink %>
<% } %>
Powered by https://cloudron.io
<% } else { %>
@@ -18,6 +27,14 @@ Powered by https://cloudron.io
A new user with email <%= user.email %> was added to <%= cloudronName %>.
</p>
<% if (inviteLink) { %>
<p>
As requested, this user has not been sent an invitation email.<br/>
<br/>
<a href="<%= inviteLink %>">Set a password and perform any configuration on behalf of the user</a>
</p>
<% } %>
<br/>
<br/>
+4 -2
View File
@@ -226,10 +226,11 @@ function sendInvite(user, invitor) {
});
}
function userAdded(user) {
function userAdded(user, inviteSent) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof inviteSent, 'boolean');
debug('Sending mail for userAdded');
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
@@ -238,6 +239,7 @@ function userAdded(user) {
var templateData = {
user: user,
inviteLink: inviteSent ? null : `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
-1
View File
@@ -30,7 +30,6 @@ function resolve(hostname, rrtype, options, callback) {
// result is an empty array if there was no error but there is no record. when you query a random
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
// type (CNAME) it is not an error and empty array
// for TXT records, result is 2d array of strings
callback(error, result);
});
}
+2
View File
@@ -33,6 +33,8 @@ exports = module.exports = {
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'boxdata/avatar.png'),
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'),
AUTO_PROVISION_FILE: path.join(config.baseDir(), 'configs/autoprovision.json'),
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs'),
// this pattern is for the cloudron logs API route to work
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/backup/app.log'),
+206 -59
View File
@@ -4,32 +4,32 @@ exports = module.exports = {
start: start,
stop: stop,
handleCertChanged: handleCertChanged,
// exported for testing
_isReady: false
handleCertChanged: handleCertChanged
};
var addons = require('./addons.js'),
apps = require('./apps.js'),
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:platform'),
execSync = require('child_process').execSync,
fs = require('fs'),
graphs = require('./graphs.js'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
locker = require('./locker.js'),
mail = require('./mail.js'),
os = require('os'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
shell = require('./shell.js'),
taskmanager = require('./taskmanager.js'),
util = require('util'),
_ = require('underscore');
var gPlatformReadyTimer = null;
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function start(callback) {
@@ -45,11 +45,13 @@ function start(callback) {
if (!existingInfra) existingInfra = { version: 'corrupt' };
}
settings.events.on(settings.PLATFORM_CONFIG_KEY, updateAddons);
// short-circuit for the restart case
if (_.isEqual(infra, existingInfra)) {
debug('platform is uptodate at version %s', infra.version);
onPlatformReady();
emitPlatformReady();
return callback();
}
@@ -61,115 +63,260 @@ function start(callback) {
async.series([
stopContainers.bind(null, existingInfra),
// mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
startAddons.bind(null, existingInfra),
removeOldImages,
startApps.bind(null, existingInfra),
graphs.startGraphite.bind(null, existingInfra),
addons.startAddons.bind(null, existingInfra),
pruneInfraImages,
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4))
], function (error) {
if (error) return callback(error);
locker.unlock(locker.OP_PLATFORM_START);
onPlatformReady();
emitPlatformReady();
callback();
});
}
function stop(callback) {
clearTimeout(gPlatformReadyTimer);
gPlatformReadyTimer = null;
exports.events = null;
taskmanager.pauseTasks(callback);
}
function onPlatformReady() {
debug('onPlatformReady: platform is ready');
exports._isReady = true;
taskmanager.resumeTasks();
applyPlatformConfig(NOOP_CALLBACK);
}
function updateAddons(platformConfig, callback) {
callback = callback || NOOP_CALLBACK;
function applyPlatformConfig(callback) {
// scale back db containers, if possible. this is retried because updating memory constraints can fail
// with failed to write to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/xx/memory.memsw.limit_in_bytes: device or resource busy
// TODO: this should possibly also rollback memory to default
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb' ], function iterator(containerName, iteratorCallback) {
const containerConfig = platformConfig[containerName];
if (!containerConfig) return iteratorCallback();
async.retry({ times: 10, interval: 5 * 60 * 1000 }, function (retryCallback) {
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return retryCallback(error);
if (!containerConfig.memory || !containerConfig.memorySwap) return iteratorCallback();
addons.updateAddonConfig(platformConfig, function (error) {
if (error) debug('Error updating addons. Will rety in 5 minutes', platformConfig, error);
retryCallback(error);
});
});
const args = `update --memory ${containerConfig.memory} --memory-swap ${containerConfig.memorySwap} ${containerName}`.split(' ');
shell.exec(`update${containerName}`, '/usr/bin/docker', args, { }, iteratorCallback);
}, callback);
}
function pruneInfraImages(callback) {
debug('pruneInfraImages: checking existing images');
function emitPlatformReady() {
// give some time for the platform to "settle". For example, mysql might still be initing the
// database dir and we cannot call service scripts until that's done.
// TODO: make this smarter to not wait for 15secs for the crash-restart case
gPlatformReadyTimer = setTimeout(function () {
debug('emitting platform ready');
gPlatformReadyTimer = null;
taskmanager.resumeTasks();
}, 15000);
}
// cannot blindly remove all unused images since redis image may not be used
let images = infra.baseImages.concat(Object.keys(infra.images).map(function (addon) { return infra.images[addon]; }));
function removeOldImages(callback) {
debug('removing old addon images');
for (let image of images) {
let output = execSync(`docker images --digests ${image.repo} --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}"`, { encoding: 'utf8' });
let lines = output.trim().split('\n');
for (let line of lines) {
if (!line) continue;
let parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
if (image.tag === parts[1]) continue; // keep
debug(`pruneInfraImages: removing unused image of ${image.repo}: ${line}`);
shell.execSync('pruneInfraImages', `docker rmi ${parts[0]}`);
}
for (var imageName in infra.images) {
if (imageName === 'redis') continue; // see #223
var image = infra.images[imageName];
debug('cleaning up images of %j', image);
var cmd = 'docker images "%s" | tail -n +2 | awk \'{ print $1 ":" $2 }\' | grep -v "%s" | xargs --no-run-if-empty docker rmi';
shell.execSync('removeOldImagesSync', util.format(cmd, image.repo, image.tag));
}
callback();
}
function stopContainers(existingInfra, callback) {
// TODO: be nice and stop addons cleanly (example, shutdown commands)
// always stop addons to restart them on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
// TODO: only nuke containers with isCloudronManaged=true
debug('stopping all containers for infra upgrade');
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker stop');
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f');
} else {
assert(typeof infra.images, 'object');
var changedAddons = [ ];
for (var imageName in infra.images) {
if (imageName === 'redis') continue; // see #223
if (infra.images[imageName].tag !== existingInfra.images[imageName].tag) changedAddons.push(imageName);
}
debug('stopContainer: stopping addons for incremental infra update: %j', changedAddons);
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
debug('stopping addons for incremental infra update: %j', changedAddons);
// ignore error if container not found (and fail later) so that this code works across restarts
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker stop || true`);
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker rm -f || true`);
shell.execSync('stopContainers', 'docker rm -f ' + changedAddons.join(' ') + ' || true');
}
callback();
}
function startGraphite(callback) {
const tag = infra.images.graphite.tag;
const dataDir = paths.PLATFORM_DATA_DIR;
const cmd = `docker run --restart=always -d --name="graphite" \
--net cloudron \
--net-alias graphite \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=graphite \
-m 75m \
--memory-swap 150m \
--dns 172.18.0.1 \
--dns-search=. \
-p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8000:8000 \
-v "${dataDir}/graphite:/app/data" \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startGraphite', cmd);
callback();
}
function startMysql(callback) {
const tag = infra.images.mysql.tag;
const dataDir = paths.PLATFORM_DATA_DIR;
const rootPassword = hat(8 * 128);
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256;
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mysql_vars.sh',
'MYSQL_ROOT_PASSWORD=' + rootPassword +'\nMYSQL_ROOT_HOST=172.18.0.1', 'utf8')) {
return callback(new Error('Could not create mysql var file:' + safe.error.message));
}
const cmd = `docker run --restart=always -d --name="mysql" \
--net cloudron \
--net-alias mysql \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mysql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-v "${dataDir}/mysql:/var/lib/mysql" \
-v "${dataDir}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startMysql', cmd);
setTimeout(callback, 5000);
}
function startPostgresql(callback) {
const tag = infra.images.postgresql.tag;
const dataDir = paths.PLATFORM_DATA_DIR;
const rootPassword = hat(8 * 128);
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256;
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/postgresql_vars.sh', 'POSTGRESQL_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
return callback(new Error('Could not create postgresql var file:' + safe.error.message));
}
const cmd = `docker run --restart=always -d --name="postgresql" \
--net cloudron \
--net-alias postgresql \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=postgresql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-v "${dataDir}/postgresql:/var/lib/postgresql" \
-v "${dataDir}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startPostgresql', cmd);
setTimeout(callback, 5000);
}
function startMongodb(callback) {
const tag = infra.images.mongodb.tag;
const dataDir = paths.PLATFORM_DATA_DIR;
const rootPassword = hat(8 * 128);
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200;
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mongodb_vars.sh', 'MONGODB_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
return callback(new Error('Could not create mongodb var file:' + safe.error.message));
}
const cmd = `docker run --restart=always -d --name="mongodb" \
--net cloudron \
--net-alias mongodb \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mongodb \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-v "${dataDir}/mongodb:/var/lib/mongodb" \
-v "${dataDir}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startMongodb', cmd);
setTimeout(callback, 5000);
}
function startAddons(existingInfra, callback) {
var startFuncs = [ ];
// always start addons on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
debug('startAddons: no existing infra or infra upgrade. starting all addons');
startFuncs.push(startGraphite, startMysql, startPostgresql, startMongodb, mail.startMail);
} else {
assert.strictEqual(typeof existingInfra.images, 'object');
if (infra.images.graphite.tag !== existingInfra.images.graphite.tag) startFuncs.push(startGraphite);
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql);
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql);
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb);
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(mail.startMail);
debug('startAddons: existing infra. incremental addon create %j', startFuncs.map(function (f) { return f.name; }));
}
async.series(startFuncs, function (error) {
if (error) return callback(error);
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(error);
updateAddons(platformConfig, callback);
});
});
}
function startApps(existingInfra, callback) {
if (existingInfra.version === 'none') { // cloudron is being restored from backup
// Infra version change strategy:
// * no existing version - restore apps
// * major versions - restore apps
// * minor versions - reconfigure apps
if (existingInfra.version === infra.version) {
debug('startApp: apps are already uptodate');
callback();
} else if (existingInfra.version === 'none' || !semver.valid(existingInfra.version) || semver.major(existingInfra.version) !== semver.major(infra.version)) {
debug('startApps: restoring installed apps');
apps.restoreInstalledApps(callback);
} else if (existingInfra.version !== infra.version) {
} else {
debug('startApps: reconfiguring installed apps');
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
apps.configureInstalledApps(callback);
} else {
debug('startApps: apps are already uptodate');
callback();
}
}
function handleCertChanged(cn) {
assert.strictEqual(typeof cn, 'string');
debug('handleCertChanged', cn);
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) {
mail.startMail(NOOP_CALLBACK);
}
+143 -256
View File
@@ -6,15 +6,11 @@ exports = module.exports = {
setFallbackCertificate: setFallbackCertificate,
getFallbackCertificate: getFallbackCertificate,
generateFallbackCertificateSync: generateFallbackCertificateSync,
setAppCertificateSync: setAppCertificateSync,
validateCertificate: validateCertificate,
getCertificate: getCertificate,
renewAll: renewAll,
renewCerts: renewCerts,
configureDefaultServer: configureDefaultServer,
@@ -26,10 +22,10 @@ exports = module.exports = {
removeAppConfigs: removeAppConfigs,
// exported for testing
_getCertApi: getCertApi
_getApi: getApi
};
var acme2 = require('./cert/acme2.js'),
var acme = require('./cert/acme.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
@@ -37,7 +33,7 @@ var acme2 = require('./cert/acme2.js'),
config = require('./config.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:reverseproxy'),
debug = require('debug')('box:certificates'),
domains = require('./domains.js'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
@@ -55,7 +51,8 @@ var acme2 = require('./cert/acme2.js'),
util = require('util');
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
NOOP_CALLBACK = function (error) { if (error) debug(error); };
function ReverseProxyError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -80,29 +77,33 @@ ReverseProxyError.INTERNAL_ERROR = 'Internal Error';
ReverseProxyError.INVALID_CERT = 'Invalid certificate';
ReverseProxyError.NOT_FOUND = 'Not Found';
function getCertApi(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (domainObject.tlsConfig.provider === 'fallback') return callback(null, fallback, { fallback: true });
domains.get(app.domain, function (error, domain) {
if (error) return callback(error);
var api = domainObject.tlsConfig.provider === 'caas' ? caas : acme2;
if (domain.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
if (domainObject.tlsConfig.provider !== 'caas') { // matches 'le-prod' or 'letsencrypt-prod'
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null;
options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
options.wildcard = !!domainObject.tlsConfig.wildcard;
}
var api = domain.tlsConfig.provider === 'caas' ? caas : acme;
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
users.getOwner(function (error, owner) {
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
var options = { };
if (domain.tlsConfig.provider === 'caas') {
options.prod = true;
} else { // acme
options.prod = domain.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
}
callback(null, api, options);
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
users.getOwner(function (error, owner) {
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
callback(null, api, options);
});
});
}
@@ -119,55 +120,30 @@ function isExpiringSync(certFilePath, hours) {
return result.status === 1; // 1 - expired 0 - not expired
}
// checks if the certificate matches the options provided by user (like wildcard, le-staging etc)
function providerMatchesSync(domainObject, certFilePath, apiOptions) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof apiOptions, 'object');
if (!fs.existsSync(certFilePath)) return false; // not found
if (apiOptions.fallback) return certFilePath.includes('.host.cert');
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
const subject = subjectAndIssuer.match(/^subject=(.*)$/m)[1];
const domain = subject.substr(subject.indexOf('=') + 1).trim(); // subject can be /CN=, CN=, CN = and other forms
const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1];
const isWildcardCert = domain.includes('*');
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt Authority');
const issuerMismatch = (apiOptions.prod && !isLetsEncryptProd) || (!apiOptions.prod && isLetsEncryptProd);
// bare domain is not part of wildcard SAN
const wildcardMismatch = (domain !== domainObject.domain) && (apiOptions.wildcard && !isWildcardCert) || (!apiOptions.wildcard && isWildcardCert);
const mismatch = issuerMismatch || wildcardMismatch;
debug(`providerMatchesSync: ${certFilePath} subject=${subject} domain=${domain} issuer=${issuer} wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} match=${!mismatch}`);
return !mismatch;
}
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
// servers certificate appears first (and not the intermediate cert)
function validateCertificate(location, domainObject, certificate) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domainObject, 'object');
assert(certificate && typeof certificate, 'object');
function validateCertificate(domain, cert, key) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
const cert = certificate.cert, key = certificate.key;
function matchesDomain(candidate) {
if (typeof candidate !== 'string') return false;
if (candidate === domain) return true;
if (candidate.indexOf('*') === 0 && candidate.slice(2) === domain.slice(domain.indexOf('.') + 1)) return true;
return false;
}
// check for empty cert and key strings
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
const fqdn = domains.fqdn(location, domainObject);
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${domain}"`, { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${fqdn}`);
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${domain}`);
// 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 });
@@ -187,52 +163,27 @@ function reload(callback) {
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
}
function generateFallbackCertificateSync(domainObject) {
assert.strictEqual(typeof domainObject, 'object');
const domain = domainObject.domain;
const certFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.cert`);
const keyFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.key`);
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
let opensslConfWithSan;
let cn = domainObject.config.hyphenatedSubdomains ? domains.parentDomain(domain) : domain;
debug(`generateFallbackCertificateSync: domain=${domainObject.domain} cn=${cn} hyphenated=${domainObject.config.hyphenatedSubdomains}`);
opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${cn}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`);
if (!safe.child_process.execSync(certCommand)) return { error: new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message) };
safe.fs.unlinkSync(configFile);
const cert = safe.fs.readFileSync(certFilePath, 'utf8');
if (!cert) return { error: safe.error };
safe.fs.unlinkSync(certFilePath);
const key = safe.fs.readFileSync(keyFilePath, 'utf8');
if (!key) return { error: safe.error };
safe.fs.unlinkSync(keyFilePath);
return { cert: cert, key: key, error: null };
}
function setFallbackCertificate(domain, fallback, callback) {
assert.strictEqual(typeof domain, 'string');
assert(fallback && typeof fallback === 'object');
assert.strictEqual(typeof fallback, 'object');
assert.strictEqual(typeof callback, 'function');
if (fallback.restricted) { // restricted certs are not backed up
debug(`setFallbackCertificate: setting restricted certs for domain ${domain}`);
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
} else {
debug(`setFallbackCertificate: setting certs for domain ${domain}`);
const certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
const keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
if (fallback) {
// backup the cert
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
} else if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${domain}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`);
if (!safe.child_process.execSync(certCommand)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
safe.fs.unlinkSync(configFile);
}
platform.handleCertChanged('*.' + domain);
@@ -252,119 +203,83 @@ function getFallbackCertificate(domain, callback) {
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
var keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath, type: 'provisioned' });
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
// check for auto-generated or user set fallback certs
certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
callback(null, { certFilePath, keyFilePath, type: 'fallback' });
}
function setAppCertificateSync(location, domainObject, certificate) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof certificate, 'object');
let fqdn = domains.fqdn(location, domainObject);
if (certificate.cert && certificate.key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), certificate.cert)) return safe.error;
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), certificate.key)) return safe.error;
} else { // remove existing cert/key
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`))) debug('Error removing key: ' + safe.error.message);
}
return null;
}
function getCertificateByHostname(hostname, domainObject, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
let certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.cert`);
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
if (hostname !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
let certName = domains.makeWildcard(hostname).replace('*.', '_.');
certFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
} else {
certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
}
callback(null);
callback(null, { certFilePath, keyFilePath });
}
function getCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
domains.get(app.domain, function (error, domainObject) {
if (error) return callback(error);
var certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.key`);
getCertificateByHostname(app.fqdn, domainObject, function (error, result) {
if (error || result) return callback(error, result);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
return getFallbackCertificate(app.domain, callback);
});
});
certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
return getFallbackCertificate(app.domain, callback);
}
function ensureCertificate(vhost, domain, auditSource, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof domain, 'string');
function ensureCertificate(app, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
domains.get(domain, function (error, domainObject) {
const vhost = app.fqdn;
var certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. user certificate already exists at %s', vhost, keyFilePath);
return callback(null, { certFilePath, keyFilePath, reason: 'user' });
}
certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. certificate already exists at %s', vhost, keyFilePath);
if (!isExpiringSync(certFilePath, 24 * 30)) return callback(null, { certFilePath, keyFilePath, reason: 'existing-le' });
debug('ensureCertificate: %s cert require renewal', vhost);
} else {
debug('ensureCertificate: %s cert does not exist', vhost);
}
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
getCertApi(domainObject, function (error, api, apiOptions) {
if (error) return callback(error);
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
getCertificateByHostname(vhost, domainObject, function (error, currentBundle) {
if (currentBundle) {
debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`);
api.getCertificate(vhost, apiOptions, function (error, certFilePath, keyFilePath) {
var errorMessage = error ? error.message : '';
if (currentBundle.certFilePath.endsWith('.user.cert')) return callback(null, currentBundle); // user certs cannot be renewed
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle);
debug(`ensureCertificate: ${vhost} cert require renewal`);
} else {
debug(`ensureCertificate: ${vhost} cert does not exist`);
}
if (error) {
debug('ensureCertificate: could not get certificate. using fallback certs', error);
mailer.certificateRenewalError(vhost, errorMessage);
}
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: vhost, errorMessage: errorMessage });
api.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) {
var errorMessage = error ? error.message : '';
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
if (!certFilePath || !keyFilePath) return getFallbackCertificate(app.domain, callback);
if (error) {
debug('ensureCertificate: could not get certificate. using fallback certs', error);
mailer.certificateRenewalError(vhost, errorMessage);
}
eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: errorMessage });
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
if (!certFilePath || !keyFilePath) return getFallbackCertificate(domain, callback);
callback(null, { certFilePath, keyFilePath, type: 'new-le' });
});
});
callback(null, { certFilePath, keyFilePath, reason: 'new-le' });
});
});
}
function writeAdminConfig(bundle, configFileName, vhost, callback) {
function configureAdminInternal(bundle, configFileName, vhost, callback) {
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
@@ -393,14 +308,15 @@ function configureAdmin(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
ensureCertificate(config.adminFqdn(), config.adminDomain(), auditSource, function (error, bundle) {
var adminApp = { domain: config.adminDomain(), fqdn: config.adminFqdn() };
ensureCertificate(adminApp, auditSource, function (error, bundle) {
if (error) return callback(error);
writeAdminConfig(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
configureAdminInternal(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
});
}
function writeAppConfig(app, bundle, callback) {
function configureAppInternal(app, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -433,7 +349,7 @@ function writeAppConfig(app, bundle, callback) {
reload(callback);
}
function writeAppRedirectConfig(app, fqdn, bundle, callback) {
function configureAppRedirect(app, fqdn, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof bundle, 'object');
@@ -469,17 +385,20 @@ function configureApp(app, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
ensureCertificate(app.fqdn, app.domain, auditSource, function (error, bundle) {
ensureCertificate({ fqdn: app.fqdn, domain: app.domain }, auditSource, function (error, bundle) {
if (error) return callback(error);
writeAppConfig(app, bundle, function (error) {
configureAppInternal(app, bundle, function (error) {
if (error) return callback(error);
async.eachSeries(app.alternateDomains, function (alternateDomain, callback) {
ensureCertificate(alternateDomain.fqdn, alternateDomain.domain, auditSource, function (error, bundle) {
// now setup alternateDomain redirects if any
async.eachSeries(app.alternateDomains, function (domain, callback) {
var fqdn = (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain;
ensureCertificate({ fqdn: fqdn, domain: domain.domain }, auditSource, function (error, bundle) {
if (error) return callback(error);
writeAppRedirectConfig(app, alternateDomain.fqdn, bundle, callback);
configureAppRedirect(app, fqdn, bundle, callback);
});
}, callback);
});
@@ -498,79 +417,47 @@ function unconfigureApp(app, callback) {
});
}
function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
var appDomains = [];
// add webadmin domain
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_ADMIN_CONFIG_FILE_NAME) });
// add app main
allApps.forEach(function (app) {
appDomains.push({ domain: app.domain, fqdn: app.fqdn, type: 'main', app: app, nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf') });
app.alternateDomains.forEach(function (alternateDomain) {
let nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}-redirect-${alternateDomain.fqdn}.conf`);
appDomains.push({ domain: alternateDomain.domain, fqdn: alternateDomain.fqdn, type: 'alternate', app: app, nginxConfigFilename: nginxConfigFilename });
});
});
if (options.domain) appDomains = appDomains.filter(function (appDomain) { return appDomain.domain === options.domain; });
async.eachSeries(appDomains, function (appDomain, iteratorCallback) {
ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle) {
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
// hack to check if the app's cert changed or not. this doesn't handle prod/staging le change since they use same file name
let currentNginxConfig = safe.fs.readFileSync(appDomain.nginxConfigFilename, 'utf8') || '';
if (currentNginxConfig.includes(bundle.certFilePath)) return iteratorCallback();
debug(`renewCerts: creating new nginx config since ${appDomain.nginxConfigFilename} does not have ${bundle.certFilePath}`);
// reconfigure since the cert changed
var configureFunc;
if (appDomain.type === 'webadmin') configureFunc = writeAdminConfig.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
else return callback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
configureFunc(function (ignoredError) {
if (ignoredError) debug('renewAll: error reconfiguring app', ignoredError);
platform.handleCertChanged(appDomain.fqdn);
iteratorCallback(); // move to next domain
});
});
});
});
}
function renewAll(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('renewAll: Checking certificates for renewal');
renewCerts({}, auditSource, callback);
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ domain: config.adminDomain(), fqdn: config.adminFqdn() }); // inject fake webadmin app
async.eachSeries(allApps, function (app, iteratorCallback) {
ensureCertificate(app, auditSource, function (error, bundle) {
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
if (bundle.reason !== 'new-le' && bundle.reason !== 'fallback') return iteratorCallback();
// reconfigure for the case where we got a renewed cert after fallback
var configureFunc = app.fqdn === config.adminFqdn() ?
configureAdminInternal.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: configureAppInternal.bind(null, app, bundle);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
platform.handleCertChanged(app.fqdn);
iteratorCallback(); // move to next app
});
});
});
});
}
function removeAppConfigs() {
for (let appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && appConfigFile !== constants.NGINX_ADMIN_CONFIG_FILE_NAME) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
for (var appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}
function configureDefaultServer(callback) {
assert.strictEqual(typeof callback, 'function');
callback = callback || NOOP_CALLBACK;
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
@@ -583,7 +470,7 @@ function configureDefaultServer(callback) {
safe.child_process.execSync(certCommand);
}
writeAdminConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
configureAdminInternal({ certFilePath, keyFilePath }, 'default.conf', '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
+24 -7
View File
@@ -4,23 +4,24 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
isUnmanaged: isUnmanaged,
scope: scope,
websocketAuth: websocketAuth
websocketAuth: websocketAuth,
verifyAppOwnership: verifyAppOwnership
};
var accesscontrol = require('../accesscontrol.js'),
apps = require('../apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
clients = require('../clients.js'),
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
ClientsError = clients.ClientsError,
config = require('../config.js'),
HttpError = require('connect-lastmile').HttpError,
LocalStrategy = require('passport-local').Strategy,
passport = require('passport'),
settings = require('../settings.js'),
users = require('../users.js'),
UsersError = users.UsersError;
@@ -142,8 +143,24 @@ function websocketAuth(requiredScopes, req, res, next) {
});
}
function isUnmanaged(req, res, next) {
if (!config.isManaged()) return next();
function verifyAppOwnership(req, res, next) {
if (req.user.admin) return next();
next(new HttpError(401, 'Managed instance does not permit this operation'));
const appCreate = !('id' in req.params);
settings.getSpacesConfig(function (error, spaces) {
if (error) return next(new HttpError(500, error));
if (!spaces.enabled) return next();
if (appCreate) return next(); // ok to install app
apps.get(req.params.id, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
if (app.ownerId !== req.user.id) return next(new HttpError(401, 'Unauthorized'));
next();
});
});
}
+8 -45
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
verifyOwnership: verifyOwnership,
getApp: getApp,
getApps: getApps,
getAppIcon: getAppIcon,
@@ -32,7 +30,6 @@ exports = module.exports = {
var apps = require('../apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:routes/apps'),
fs = require('fs'),
HttpError = require('connect-lastmile').HttpError,
@@ -47,25 +44,6 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function verifyOwnership(req, res, next) {
if (req.user.admin) return next();
if (!config.isSpacesEnabled()) return next();
const appCreate = !('id' in req.params);
if (appCreate) return next(); // ok to install app
apps.get(req.params.id, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
if (app.ownerId !== req.user.id) return next(new HttpError(401, 'Unauthorized'));
next();
});
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
@@ -139,19 +117,9 @@ function installApp(req, res, next) {
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
if ('alternateDomains' in data) {
if (!Array.isArray(data.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
}
if ('env' in data) {
if (!data.env || typeof data.env !== 'object') return next(new HttpError(400, 'env must be an object'));
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
}
debug('Installing app :%j', data);
apps.install(data, req.user, auditSource(req), function (error, app) {
apps.install(data, auditSource(req), function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
@@ -159,7 +127,7 @@ function installApp(req, res, next) {
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, error.message));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, app));
@@ -199,14 +167,9 @@ function configureApp(req, res, next) {
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
}
if ('env' in data) {
if (!data.env || typeof data.env !== 'object') return next(new HttpError(400, 'env must be an object'));
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
}
debug('Configuring app id:%s data:%j', req.params.id, data);
apps.configure(req.params.id, data, req.user, auditSource(req), function (error) {
apps.configure(req.params.id, data, auditSource(req), function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -256,7 +219,7 @@ function cloneApp(req, res, next) {
if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
apps.clone(req.params.id, data, req.user, auditSource(req), function (error, result) {
apps.clone(req.params.id, data, auditSource(req), function (error, result) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -280,7 +243,7 @@ function backupApp(req, res, next) {
apps.backup(req.params.id, function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(503, error));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { }));
@@ -513,7 +476,7 @@ function execWebSocket(req, res, next) {
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
debug('Connected to terminal');
console.log('Connected to terminal');
req.clearTimeout();
@@ -521,7 +484,7 @@ function execWebSocket(req, res, next) {
duplexStream.on('end', function () { ws.close(); });
duplexStream.on('close', function () { ws.close(); });
duplexStream.on('error', function (error) {
debug('duplexStream error:', error);
console.error('duplexStream error:', error);
});
duplexStream.on('data', function (data) {
if (ws.readyState !== WebSocket.OPEN) return;
@@ -529,7 +492,7 @@ function execWebSocket(req, res, next) {
});
ws.on('error', function (error) {
debug('websocket error:', error);
console.error('websocket error:', error);
});
ws.on('message', function (msg) {
duplexStream.write(msg);
+1 -1
View File
@@ -24,7 +24,7 @@ function get(req, res, next) {
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, page, perPage, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { backups: result }));
+1 -2
View File
@@ -80,9 +80,8 @@ function addToken(req, res, next) {
var data = req.body;
var expiresAt = data.expiresAt ? parseInt(data.expiresAt, 10) : Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
if (isNaN(expiresAt) || expiresAt <= Date.now()) return next(new HttpError(400, 'expiresAt must be a timestamp in the future'));
if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, { name: req.body.name || '' }, function (error, result) {
clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { token: result }));
+1 -13
View File
@@ -10,8 +10,7 @@ exports = module.exports = {
feedback: feedback,
checkForUpdates: checkForUpdates,
getLogs: getLogs,
getLogStream: getLogStream,
getStatus: getStatus,
getLogStream: getLogStream
};
var appstore = require('../appstore.js'),
@@ -76,9 +75,6 @@ function getUpdateInfo(req, res, next) {
}
function checkForUpdates(req, res, next) {
// it can take a while sometimes to get all the app updates one by one
req.clearTimeout();
async.series([
updateChecker.checkAppUpdates,
updateChecker.checkBoxUpdates
@@ -170,11 +166,3 @@ function getLogStream(req, res, next) {
logStream.on('error', res.end.bind(res, null));
});
}
function getStatus(req, res, next) {
cloudron.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
}
+1 -2
View File
@@ -24,8 +24,7 @@ function login(req, res, next) {
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
}
const auditSource = { authType: 'cli', ip: ip };
clients.issueDeveloperToken(user, auditSource, function (error, result) {
clients.issueDeveloperToken(user, ip, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
+14 -83
View File
@@ -5,11 +5,7 @@ exports = module.exports = {
get: get,
getAll: getAll,
update: update,
del: del,
renewCerts: renewCerts,
verifyDomainLock: verifyDomainLock
del: del
};
var assert = require('assert'),
@@ -18,56 +14,23 @@ var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
// this code exists for the hosting provider edition
function verifyDomainLock(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
if (domains.isLocked(req.params.domain)) return next(new HttpError(423, 'This domain is locked'));
next();
}
function add(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be a string'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('hyphenatedSubdomains' in req.body.config && typeof req.body.config.hyphenatedSubdomains !== 'boolean') return next(new HttpError(400, 'hyphenatedSubdomains must be a boolean'));
if ('wildcard' in req.body.config && typeof req.body.config.wildcard !== 'boolean') return next(new HttpError(400, 'wildcard must be a boolean'));
if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
if (req.body.fallbackCertificate) {
let fallbackCertificate = req.body.fallbackCertificate;
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('restricted' in fallbackCertificate && typeof fallbackCertificate.restricted !== 'boolean') return next(new HttpError(400, 'fallbackCertificate.restricted must be a boolean'));
}
if ('tlsConfig' in req.body) {
if (!req.body.tlsConfig || typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string') return next(new HttpError(400, 'tlsConfig.provider must be a string'));
}
if (req.body.fallbackCertificate && (!req.body.cert || typeof req.body.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (req.body.fallbackCertificate && (!req.body.key || typeof req.body.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
let data = {
zoneName: req.body.zoneName || '',
provider: req.body.provider,
config: req.body.config,
fallbackCertificate: req.body.fallbackCertificate || null,
tlsConfig: req.body.tlsConfig || { provider: 'letsencrypt-prod' }
};
domains.add(req.body.domain, data, auditSource(req), function (error) {
domains.add(req.body.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === DomainsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
@@ -103,37 +66,18 @@ function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be an object'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('hyphenatedSubdomains' in req.body.config && typeof req.body.config.hyphenatedSubdomains !== 'boolean') return next(new HttpError(400, 'hyphenatedSubdomains must be a boolean'));
if ('wildcard' in req.body.config && typeof req.body.config.wildcard !== 'boolean') return next(new HttpError(400, 'wildcard must be a boolean'));
if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
if (req.body.fallbackCertificate) {
let fallbackCertificate = req.body.fallbackCertificate;
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('restricted' in fallbackCertificate && typeof fallbackCertificate.restricted !== 'boolean') return next(new HttpError(400, 'fallbackCertificate.restricted must be a boolean'));
}
if ('tlsConfig' in req.body) {
if (!req.body.tlsConfig || typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string') return next(new HttpError(400, 'tlsConfig.provider must be a string'));
}
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.cert || typeof req.body.fallbackCertificate.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.key || typeof req.body.fallbackCertificate.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
let data = {
zoneName: req.body.zoneName || '',
provider: req.body.provider,
config: req.body.config,
fallbackCertificate: req.body.fallbackCertificate || null,
tlsConfig: req.body.tlsConfig || { provider: 'letsencrypt-prod' }
};
domains.update(req.params.domain, data, auditSource(req), function (error) {
domains.update(req.params.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
@@ -146,7 +90,7 @@ function update(req, res, next) {
function del(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
domains.del(req.params.domain, auditSource(req), function (error) {
domains.del(req.params.domain, function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainsError.IN_USE) return next(new HttpError(409, 'Domain is still in use. Remove all apps and mailboxes using this domain'));
if (error) return next(new HttpError(500, error));
@@ -154,16 +98,3 @@ function del(req, res, next) {
next(new HttpSuccess(204));
});
}
function renewCerts(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
domains.renewCerts(req.params.domain, auditSource(req), function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
next(new HttpSuccess(202, {}));
}
+5 -15
View File
@@ -44,11 +44,6 @@ var assert = require('assert'),
var mailProxy = middleware.proxy(url.parse('http://127.0.0.1:2020'));
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function getDomain(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
@@ -91,13 +86,8 @@ function setDnsRecords(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.domain, 'string');
// can take a setup all the DNS entries. this is mostly because some backends try to list DNS entries (DO)
// for upsert and this takes a lot of time
req.clearTimeout();
mail.setDnsRecords(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201));
@@ -190,7 +180,7 @@ function setMailEnabled(req, res, next) {
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
mail.setMailEnabled(req.params.domain, !!req.body.enabled, auditSource(req), function (error) {
mail.setMailEnabled(req.params.domain, !!req.body.enabled, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === MailError.BILLING_REQUIRED) return next(new HttpError(402, error.message));
@@ -243,7 +233,7 @@ function addMailbox(req, res, next) {
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
mail.addMailbox(req.body.name, req.params.domain, req.body.userId, auditSource(req), function (error) {
mail.addMailbox(req.body.name, req.params.domain, req.body.userId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
@@ -272,7 +262,7 @@ function removeMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
mail.removeMailbox(req.params.name, req.params.domain, auditSource(req), function (error) {
mail.removeMailbox(req.params.name, req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
@@ -358,7 +348,7 @@ function addList(req, res, next) {
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
}
mail.addList(req.body.name, req.params.domain, req.body.members, auditSource(req), function (error) {
mail.addList(req.body.name, req.params.domain, req.body.members, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, 'list already exists'));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
@@ -391,7 +381,7 @@ function removeList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
mail.removeList(req.params.name, req.params.domain, auditSource(req), function (error) {
mail.removeList(req.params.domain, req.params.name, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
+5 -12
View File
@@ -91,7 +91,7 @@ function initialize() {
authcodedb.del(code, function (error) {
if(error) return callback(error);
clients.addTokenByUserId(client.id, authCode.userId, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
clients.addTokenByUserId(client.id, authCode.userId, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
if (error) return callback(error);
debug('exchange: new access token for client %s user %s token %s', client.id, authCode.userId, result.accessToken.slice(0, 6)); // partial token for security
@@ -104,7 +104,7 @@ function initialize() {
// implicit token grant that skips issuing auth codes. this is used by our webadmin
gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client, user, ares, callback) {
clients.addTokenByUserId(client.id, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
clients.addTokenByUserId(client.id, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
if (error) return callback(error);
debug('grant token: new access token for client %s user %s token %s', client.id, user.id, result.accessToken.slice(0, 6)); // partial token for security
@@ -362,13 +362,10 @@ function accountSetup(req, res, next) {
// setPassword clears the resetToken
users.setPassword(userObject.id, req.body.password, function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error) return next(new HttpError(500, error));
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
});
res.redirect(config.adminOrigin());
});
});
});
@@ -412,11 +409,7 @@ function passwordReset(req, res, next) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(406, error.message));
if (error) return next(new HttpError(500, error));
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
});
res.redirect(config.adminOrigin());
});
});
}
+11 -33
View File
@@ -25,15 +25,11 @@ exports = module.exports = {
getPlatformConfig: getPlatformConfig,
setPlatformConfig: setPlatformConfig,
getDynamicDnsConfig: getDynamicDnsConfig,
setDynamicDnsConfig: setDynamicDnsConfig,
setRegistryConfig: setRegistryConfig
setSpacesConfig: setSpacesConfig,
getSpacesConfig: getSpacesConfig
};
var assert = require('assert'),
docker = require('../docker.js'),
DockerError = docker.DockerError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
@@ -162,7 +158,6 @@ function setBackupConfig(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
if (typeof req.body.intervalSecs !== 'number') return next(new HttpError(400, 'intervalSecs is required'));
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('syncConcurrency' in req.body) {
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
@@ -176,7 +171,7 @@ function setBackupConfig(req, res, next) {
settings.setBackupConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
@@ -204,35 +199,33 @@ function setPlatformConfig(req, res, next) {
settings.setPlatformConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function getDynamicDnsConfig(req, res, next) {
settings.getDynamicDnsConfig(function (error, enabled) {
function getSpacesConfig(req, res, next) {
settings.getSpacesConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { enabled: enabled }));
next(new HttpSuccess(200, config));
});
}
function setDynamicDnsConfig(req, res, next) {
function setSpacesConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled boolean is required'));
settings.setDynamicDnsConfig(req.body.enabled, function (error) {
settings.setSpacesConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function getAppstoreConfig(req, res, next) {
settings.getAppstoreConfig(function (error, result) {
if (error) return next(new HttpError(500, error));
@@ -254,7 +247,7 @@ function setAppstoreConfig(req, res, next) {
settings.setAppstoreConfig(options, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(406, error.message));
if (error) return next(new HttpError(500, error));
settings.getAppstoreConfig(function (error, result) {
@@ -264,18 +257,3 @@ function setAppstoreConfig(req, res, next) {
});
});
}
function setRegistryConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.serveraddress !== 'string') return next(new HttpError(400, 'serveraddress is required'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username is required'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
docker.setRegistryConfig(req.body, function (error) {
if (error && error.reason === DockerError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
+24 -27
View File
@@ -3,9 +3,10 @@
exports = module.exports = {
providerTokenAuth: providerTokenAuth,
setupTokenAuth: setupTokenAuth,
provision: provision,
dnsSetup: dnsSetup,
activate: activate,
restore: restore
restore: restore,
getStatus: getStatus,
};
var assert = require('assert'),
@@ -53,7 +54,7 @@ function setupTokenAuth(req, res, next) {
caas.verifySetupToken(req.query.setupToken, function (error) {
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CaasError.INVALID_TOKEN) return next(new HttpError(401, 'Invalid token'));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
@@ -61,29 +62,20 @@ function setupTokenAuth(req, res, next) {
});
}
function provision(req, res, next) {
function dnsSetup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.dnsConfig || typeof req.body.dnsConfig !== 'object') return next(new HttpError(400, 'dnsConfig is required'));
if (typeof req.body.provider !== 'string' || !req.body.provider) return next(new HttpError(400, 'provider is required'));
if (typeof req.body.domain !== 'string' || !req.body.domain) return next(new HttpError(400, 'domain is required'));
if (typeof req.body.adminFqdn !== 'string' || !req.body.adminFqdn) return next(new HttpError(400, 'adminFqdn is required'));
const dnsConfig = req.body.dnsConfig;
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if (typeof dnsConfig.provider !== 'string' || !dnsConfig.provider) return next(new HttpError(400, 'provider is required'));
if (typeof dnsConfig.domain !== 'string' || !dnsConfig.domain) return next(new HttpError(400, 'domain is required'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be an object'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
if ('zoneName' in dnsConfig && typeof dnsConfig.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if (!dnsConfig.config || typeof dnsConfig.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('tlsConfig' in dnsConfig && typeof dnsConfig.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be an object'));
if (dnsConfig.tlsConfig && (!dnsConfig.tlsConfig.provider || typeof dnsConfig.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
// TODO: validate subfields of these objects
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
// it can take sometime to setup DNS, register cloudron
req.clearTimeout();
setup.provision(dnsConfig, req.body.autoconf || {}, auditSource(req), function (error) {
setup.dnsSetup(req.body.adminFqdn.toLowerCase(), req.body.domain.toLowerCase(), req.body.zoneName || '', req.body.provider, req.body.config, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
@@ -93,6 +85,14 @@ function provision(req, res, next) {
});
}
function getStatus(req, res, next) {
setup.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
}
function activate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
@@ -120,7 +120,7 @@ function activate(req, res, next) {
caas.setupDone(req.query.setupToken, function (error) {
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CaasError.INVALID_TOKEN) return next(new HttpError(401, 'Invalid token'));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
@@ -143,14 +143,11 @@ function restore(req, res, next) {
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
// TODO: validate subfields of these objects
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
setup.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, auditSource(req), function (error) {
setup.restore(backupConfig, req.body.backupId, req.body.version, function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error && error.reason === SetupError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
+3 -21
View File
@@ -3,15 +3,10 @@
exports = module.exports = {
backup: backup,
update: update,
retire: retire,
importAppDatabase: importAppDatabase
retire: retire
};
var apps = require('../apps.js'),
AppsError = apps.AppsError,
addons = require('../addons.js'),
backups = require('../backups.js'),
var backups = require('../backups.js'),
BackupsError = require('../backups.js').BackupsError,
cloudron = require('../cloudron.js'),
debug = require('debug')('box:routes/sysadmin'),
@@ -53,21 +48,8 @@ function retire(req, res, next) {
debug('triggering retire');
cloudron.retire('migrate', { }, function (error) {
if (error) debug('Retire failed.', error);
if (error) console.error('Retire failed.', error);
});
next(new HttpSuccess(202, {}));
}
function importAppDatabase(req, res, next) {
apps.get(req.params.id, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
addons.importAppDatabase(app, req.query.addon || '', function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
});
}
+44 -35
View File
@@ -15,6 +15,7 @@ var accesscontrol = require('../../accesscontrol.js'),
clients = require('../../clients.js'),
config = require('../../config.js'),
constants = require('../../constants.js'),
apphealthmonitor = require('../../apphealthmonitor.js'),
database = require('../../database.js'),
docker = require('../../docker.js').connection,
expect = require('expect.js'),
@@ -27,7 +28,6 @@ var accesscontrol = require('../../accesscontrol.js'),
nock = require('nock'),
path = require('path'),
paths = require('../../paths.js'),
platform = require('../../platform.js'),
safe = require('safetydance'),
server = require('../../server.js'),
settings = require('../../settings.js'),
@@ -216,7 +216,7 @@ function startBox(done) {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, user_1_id, 'test-client-id', Date.now() + 1000000, accesscontrol.SCOPE_APPS_READ, '', callback);
tokendb.add(token_1, user_1_id, 'test-client-id', Date.now() + 1000000, accesscontrol.SCOPE_ANY, callback);
},
function (callback) {
@@ -234,16 +234,13 @@ function startBox(done) {
}
return false;
}, callback);
},
function (callback) {
async.retry({ times: 50, interval: 10000 }, function (retryCallback) {
if (platform._isReady) return retryCallback();
console.log('Platform not ready yet, retry in 10 secs');
retryCallback('Platform not ready yet');
}, callback);
}
], done);
], function (error) {
if (error) return done(error);
console.log('This test can take ~40 seconds to start as it waits for infra to be ready');
setTimeout(done, 40000);
});
}
function stopBox(done) {
@@ -254,6 +251,7 @@ function stopBox(done) {
// db is not cleaned up here since it's too late to call it after server.stop. if called before server.stop taskmanager apptasks are unhappy :/
async.series([
apphealthmonitor.stop,
taskmanager.stopPendingTasks,
taskmanager.waitForPendingTasks,
appdb._clear,
@@ -361,7 +359,7 @@ describe('App API', function () {
.send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.contain('my is reserved');
expect(res.body.message).to.eql('my is reserved');
done();
});
});
@@ -372,7 +370,7 @@ describe('App API', function () {
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.contain(constants.API_LOCATION + ' is reserved');
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
done();
});
});
@@ -383,7 +381,7 @@ describe('App API', function () {
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: 23, accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.contain('portBindings must be an object');
expect(res.body.message).to.eql('portBindings must be an object');
done();
});
});
@@ -394,7 +392,7 @@ describe('App API', function () {
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.contain('accessRestriction is required');
expect(res.body.message).to.eql('accessRestriction is required');
done();
});
});
@@ -405,7 +403,7 @@ describe('App API', function () {
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, accessRestriction: '', domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.contain('accessRestriction is required');
expect(res.body.message).to.eql('accessRestriction is required');
done();
});
});
@@ -449,7 +447,7 @@ describe('App API', function () {
it('app install succeeds with purchase', function (done) {
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons') >= 0; }, { 'domain': DOMAIN_0.domain }).reply(201, { cloudron: { id: CLOUDRON_ID } });
var fake2 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID, 'manifestId': APP_MANIFEST.id }).reply(201, { });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
settings.setAppstoreConfig({ userId: user_1_id, token: USER_1_APPSTORE_TOKEN }, function (error) {
if (error) return done(error);
@@ -579,7 +577,7 @@ describe('App API', function () {
it('app install succeeds again', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID, 'manifestId': APP_MANIFEST.id }).reply(201, { });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
@@ -594,34 +592,44 @@ describe('App API', function () {
});
});
it('app install fails with developer token', function (done) {
it('app install succeeds without password but developer token', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
// overwrite non dev token
token = result.body.accessToken;
token = result.body.token;
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(424); // appstore purchase external error
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
done();
});
});
});
it('can uninstall app without password but developer token', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
});
});
describe('App installation', function () {
this.timeout(100000);
var apiHockInstance = hock.createHock({ throwOnUnmatched: false });
var apiHockServer;
var validCert1, validKey1;
@@ -634,6 +642,7 @@ describe('App installation', function () {
async.series([
startBox,
apphealthmonitor.start,
function (callback) {
apiHockInstance
@@ -663,7 +672,7 @@ describe('App installation', function () {
it('can install test app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID, 'manifestId': APP_MANIFEST.id }).reply(201, { });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var count = 0;
function checkInstallStatus() {
@@ -690,7 +699,7 @@ describe('App installation', function () {
});
});
xit('installation - image created', function (done) {
it('installation - image created', function (done) {
expect(imageCreated).to.be.ok();
done();
});
@@ -735,13 +744,7 @@ describe('App installation', function () {
it('installation - volume created', function (done) {
expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP_ID));
let volume = docker.getVolume(APP_ID + '-localstorage');
volume.inspect(function (error, volume) {
expect(error).to.be(null);
expect(volume.Labels.appId).to.eql(APP_ID);
expect(volume.Options.device).to.eql(paths.APPS_DATA_DIR + '/' + APP_ID + '/data');
done();
});
done();
});
it('installation - http is up and running', function (done) {
@@ -777,7 +780,13 @@ describe('App installation', function () {
it('installation - running container has volume mounted', function (done) {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
expect(error).to.not.be.ok();
expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Type).to.eql('volume');
// support newer docker versions
if (data.Volumes) {
expect(data.Volumes['/app/data']).to.eql(paths.APPS_DATA_DIR + '/' + APP_ID + '/data');
} else {
expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Source).to.eql(paths.APPS_DATA_DIR + '/' + APP_ID + '/data');
}
done();
});
@@ -906,7 +915,7 @@ describe('App installation', function () {
if (!err || err.code !== 'ECONNREFUSED') return setTimeout(waitForAppToDie, 500);
// wait for app status to be updated
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID).query({ access_token: token }).end(function (error, result) {
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID).query({ access_token: token_1 }).end(function (error, result) {
if (error || result.statusCode !== 200 || result.body.runState !== 'stopped') return setTimeout(waitForAppToDie, 500);
done();
});
+1 -3
View File
@@ -29,8 +29,6 @@ const DOMAIN_0 = {
tlsConfig: { provider: 'fallback' }
};
let AUDIT_SOURCE = { ip: '1.2.3.4' };
var token = null, ownerId = null;
function setup(done) {
@@ -40,7 +38,7 @@ function setup(done) {
async.series([
server.start,
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
+4 -4
View File
@@ -14,6 +14,7 @@ var appdb = require('../../appdb.js'),
expect = require('expect.js'),
locker = require('../../locker.js'),
nock = require('nock'),
os = require('os'),
superagent = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js'),
@@ -34,8 +35,6 @@ const DOMAIN_0 = {
tlsConfig: { provider: 'fallback' }
};
let AUDIT_SOURCE = { ip: '1.2.3.4' };
var token = null, ownerId = null;
var gSudoOriginal = null;
function injectShellMock() {
@@ -51,6 +50,7 @@ function setup(done) {
nock.cleanAll();
config._reset();
config.set('provider', 'caas');
config.setVersion('1.2.3');
async.series([
server.start.bind(server),
@@ -58,7 +58,7 @@ function setup(done) {
database._clear,
settingsdb.set.bind(null, settings.CAAS_CONFIG_KEY, JSON.stringify({ boxId: 'BOX_ID', token: 'ACCESS_TOKEN2' })),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/BOX_ID/setup/verify?setupToken=somesetuptoken').reply(200, {});
@@ -122,7 +122,7 @@ describe('Caas', function () {
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'strong#A3asdf', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(424);
expect(result.statusCode).to.equal(503);
expect(scope.isDone()).to.be.ok();
done();
});
+1 -1
View File
@@ -167,7 +167,7 @@ describe('Cloudron', function () {
userId_1 = result.body.id;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, 'cloudron', '', callback);
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, 'cloudron', callback);
});
}
], done);
+2 -2
View File
@@ -63,7 +63,7 @@ function setup(done) {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, '', callback);
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, callback);
}
], done);
@@ -141,7 +141,7 @@ describe('Eventlog API', function () {
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(2);
expect(result.body.eventlogs.length).to.equal(1);
done();
});
+1 -1
View File
@@ -70,7 +70,7 @@ function setup(done) {
userId_1 = result.body.id;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, '', callback);
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, callback);
});
}
], done);
+5 -61
View File
@@ -10,6 +10,7 @@ var async = require('async'),
database = require('../../database.js'),
expect = require('expect.js'),
mail = require('../../mail.js'),
domains = require('../../domains.js'),
maildb = require('../../maildb.js'),
server = require('../../server.js'),
superagent = require('superagent'),
@@ -17,15 +18,6 @@ var async = require('async'),
var SERVER_URL = 'http://localhost:' + config.get('port');
const ADMIN_DOMAIN = {
domain: 'admin.com',
zoneName: 'admin.com',
config: {},
provider: 'noop',
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
const DOMAIN_0 = {
domain: 'example-mail-test.com',
zoneName: 'example-mail-test.com',
@@ -41,21 +33,13 @@ var userId = '';
function setup(done) {
config._reset();
config.setFqdn(DOMAIN_0.domain);
config.setAdminFqdn('my.example-mail-test.com');
async.series([
server.start.bind(null),
database._clear.bind(null),
function dnsSetup(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: ADMIN_DOMAIN.provider, domain: ADMIN_DOMAIN.domain, config: ADMIN_DOMAIN.config } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
callback();
});
},
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
@@ -73,17 +57,6 @@ function setup(done) {
});
},
function createDomain(callback) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send(DOMAIN_0)
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
callback();
});
},
function getUserId(callback) {
userdb.getByUsername(USERNAME, function (error, result) {
expect(error).to.not.be.ok();
@@ -205,17 +178,7 @@ describe('Mail API', function () {
});
});
it('cannot delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + ADMIN_DOMAIN.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('can delete admin mail domain', function (done) {
it('can delete domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
@@ -445,25 +408,6 @@ describe('Mail API', function () {
});
});
it('succeeds with modified DMARC1 values', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC1; p=reject; rua=mailto:rua@example.com; pct=100']];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.status).to.eql(true);
expect(res.body.dns.dmarc.value).to.eql('v=DMARC1; p=reject; rua=mailto:rua@example.com; pct=100');
done();
});
});
it('succeeds with all correct records', function (done) {
clearDnsAnswerQueue();
+2 -6
View File
@@ -30,8 +30,6 @@ var accesscontrol = require('../../accesscontrol.js'),
var SERVER_URL = 'http://localhost:' + config.get('port');
let AUDIT_SOURCE = { ip: '1.2.3.4' };
describe('OAuth2', function () {
describe('flow', function () {
@@ -210,7 +208,7 @@ describe('OAuth2', function () {
async.series([
server.start,
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
clientdb.add.bind(null, CLIENT_0.id, CLIENT_0.appId, CLIENT_0.type, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope),
clientdb.add.bind(null, CLIENT_1.id, CLIENT_1.appId, CLIENT_1.type, CLIENT_1.clientSecret, CLIENT_1.redirectURI, CLIENT_1.scope),
clientdb.add.bind(null, CLIENT_2.id, CLIENT_2.appId, CLIENT_2.type, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope),
@@ -1483,11 +1481,9 @@ describe('Password', function () {
it('succeeds', function (done) {
var scope = nock(config.adminOrigin())
.filteringPath(function (path) {
path = path.replace(/accessToken=[^&]*/, 'accessToken=token');
path = path.replace(/expiresAt=[^&]*/, 'expiresAt=1234');
return path;
})
.get('/?accessToken=token&expiresAt=1234').reply(200, {});
.get('/').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: '12345678', email: USER_0.email, resetToken: USER_0.resetToken })
+1 -1
View File
@@ -115,7 +115,7 @@ describe('Profile API', function () {
var token = tokendb.generateToken();
var expires = Date.now() - 2000; // 1 sec
tokendb.add(token, user_0.id, null, expires, 'profile', 'tokenname', function (error) {
tokendb.add(token, user_0.id, null, expires, 'profile', function (error) {
expect(error).to.not.be.ok();
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
+151 -128
View File
@@ -20,6 +20,7 @@ var token = null;
function setup(done) {
config._reset();
config.setVersion('1.2.3');
async.series([
server.start,
@@ -39,212 +40,234 @@ describe('REST API', function () {
after(cleanup);
it('dns setup fails without provider', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { domain: DOMAIN, config: {} } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('dns setup fails with invalid provider', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'foobar', domain: DOMAIN, config: {} } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'foobar', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('dns setup fails with missing domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', config: {} } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('dns setup fails with invalid domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: '.foo', config: {} } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: '.foo', adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('dns setup fails with missing adminFqdn', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid adminFqdn', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my', config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid config', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, config: 'not an object' } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: 'not an object' })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('dns setup fails with invalid zoneName', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, config: {}, zoneName: 1337 } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: {}, zoneName: 1337 })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('dns setup fails with invalid tlsConfig', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, config: {}, tlsConfig: 'foobar' } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: {}, tlsConfig: 'foobar' })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('dns setup fails with invalid tlsConfig provider', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, config: {}, tlsConfig: { provider: 1337 } } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: {}, tlsConfig: { provider: 1337 } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('dns setup succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
done();
});
done();
});
});
it('dns setup twice fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(409);
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(409);
done();
});
done();
});
});
it('activation fails without username', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
.query({ setupToken: 'somesetuptoken' })
.send({ password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('activation fails with invalid username', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: '?this.is-not!valid', password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
.query({ setupToken: 'somesetuptoken' })
.send({ username: '?this.is-not!valid', password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('activation fails without email', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('activation fails with invalid email', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: 'notanemail' })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: 'notanemail' })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('activation fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('activation fails with invalid password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: 'short', email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: 'short', email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
done();
});
});
it('activation succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
// stash token for further use
token = result.body.token;
// stash token for further use
token = result.body.token;
done();
});
done();
});
});
it('activating twice fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(409);
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(409);
done();
});
done();
});
});
it('does not crash with invalid JSON', function (done) {
+6 -6
View File
@@ -82,7 +82,7 @@ describe('Settings API', function () {
it('can set app_autoupdate_pattern', function (done) {
var eventPattern = null;
settings.on(settings.APP_AUTOUPDATE_PATTERN_KEY, function (pattern) {
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, function (pattern) {
eventPattern = pattern;
});
@@ -98,7 +98,7 @@ describe('Settings API', function () {
it('can set app_autoupdate_pattern to never', function (done) {
var eventPattern = null;
settings.on(settings.APP_AUTOUPDATE_PATTERN_KEY, function (pattern) {
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, function (pattern) {
eventPattern = pattern;
});
@@ -145,7 +145,7 @@ describe('Settings API', function () {
it('can set box_autoupdate_pattern', function (done) {
var eventPattern = null;
settings.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, function (pattern) {
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, function (pattern) {
eventPattern = pattern;
});
@@ -161,7 +161,7 @@ describe('Settings API', function () {
it('can set box_autoupdate_pattern to never', function (done) {
var eventPattern = null;
settings.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, function (pattern) {
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, function (pattern) {
eventPattern = pattern;
});
@@ -320,7 +320,7 @@ describe('Settings API', function () {
.send({ userId: 'nebulon', token: 'sometoken' })
.end(function (err, res) {
expect(scope.isDone()).to.be.ok();
expect(res.statusCode).to.equal(424);
expect(res.statusCode).to.equal(406);
expect(res.body.message).to.equal('invalid appstore token');
done();
@@ -350,7 +350,7 @@ describe('Settings API', function () {
.send({ userId: 'nebulon', token: 'sometoken' })
.end(function (err, res) {
expect(scope.isDone()).to.be.ok();
expect(res.statusCode).to.equal(424);
expect(res.statusCode).to.equal(406);
expect(res.body.message).to.equal('wrong user');
done();
+3 -4
View File
@@ -29,16 +29,15 @@ const DOMAIN_0 = {
tlsConfig: { provider: 'fallback' }
};
let AUDIT_SOURCE = { ip: '1.2.3.4' };
function setup(done) {
config._reset();
config.setFqdn(DOMAIN_0.domain);
config.setVersion('1.2.3');
async.series([
server.start,
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
@@ -90,4 +89,4 @@ describe('Internal API', function () {
});
});
});
});
});
+47 -90
View File
@@ -8,6 +8,7 @@
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
tokendb = require('../../tokendb.js'),
@@ -16,8 +17,7 @@ var accesscontrol = require('../../accesscontrol.js'),
mail = require('../../mail.js'),
mailer = require('../../mailer.js'),
superagent = require('superagent'),
server = require('../../server.js'),
users = require('../../users.js');
server = require('../../server.js');
const SERVER_URL = 'http://localhost:' + config.get('port');
@@ -30,8 +30,6 @@ const DOMAIN_0 = {
tlsConfig: { provider: 'fallback' }
};
let AUDIT_SOURCE = { ip: '1.2.3.4' };
const USERNAME_0 = 'superaDmIn', PASSWORD = 'Foobar?1337', EMAIL_0 = 'silLY@me.com', EMAIL_0_NEW = 'stupID@me.com', DISPLAY_NAME_0_NEW = 'New Name';
const USERNAME_1 = 'userTheFirst', EMAIL_1 = 'taO@zen.mac';
const USERNAME_2 = 'userTheSecond', EMAIL_2 = 'USER@foo.bar', EMAIL_2_NEW = 'happy@ME.com';
@@ -47,7 +45,7 @@ function setup(done) {
server.start,
database._clear,
mailer._clearMailQueue,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
mail.addDomain.bind(null, DOMAIN_0.domain)
], function (error) {
expect(error).to.not.be.ok();
@@ -85,7 +83,7 @@ describe('Users API', function () {
this.timeout(5000);
var user_0, user_1, user_2, user_4;
var token = null, userToken = null;
var token = null;
var token_1 = tokendb.generateToken();
before(setup);
@@ -178,7 +176,7 @@ describe('Users API', function () {
var token = tokendb.generateToken();
var expires = Date.now() + 2000; // 1 sec
tokendb.add(token, user_0.id, null, expires, accesscontrol.SCOPE_PROFILE, 'tokenname', function (error) {
tokendb.add(token, user_0.id, null, expires, accesscontrol.SCOPE_PROFILE, function (error) {
expect(error).to.not.be.ok();
setTimeout(function () {
@@ -268,9 +266,11 @@ describe('Users API', function () {
});
it('cannot create user without email', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1 })
.send({ username: USERNAME_1, invite: true })
.end(function (error, result) {
expect(error).to.be.ok();
expect(result.statusCode).to.equal(400);
@@ -279,63 +279,46 @@ describe('Users API', function () {
});
it('create second user succeeds', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, email: EMAIL_1 })
.send({ username: USERNAME_1, email: EMAIL_1, invite: true })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
user_1 = result.body;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, user_1.id, 'test-client-id', Date.now() + 10000, accesscontrol.SCOPE_PROFILE, 'fromtest', done);
checkMails(2, function () {
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, user_1.id, 'test-client-id', Date.now() + 10000, accesscontrol.SCOPE_PROFILE, done);
});
});
});
it('reinvite unknown user fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1+USERNAME_1 + '/create_invite')
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1+USERNAME_1 + '/invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.be.an(Error);
expect(res.statusCode).to.equal(404);
done();
checkMails(0, done);
});
});
it('send invite without creating invite fails succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/send_invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.be.an(Error);
expect(res.statusCode).to.equal(409);
done();
});
});
it('reinvite second user succeeds', function (done) {
mailer._clearMailQueue();
it('create invite second user succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/create_invite')
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.not.be.ok();
expect(res.statusCode).to.equal(200);
expect(res.body.resetToken).to.be.ok();
done();
});
});
it('can send invite', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/send_invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.be(null);
expect(res.statusCode).to.equal(200);
checkMails(1, done);
});
});
@@ -401,12 +384,12 @@ describe('Users API', function () {
});
});
it('create user missing username succeeds', function (done) {
it('create user missing username fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ email: `unnamed${EMAIL_2}` })
.send({ email: EMAIL_2 })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(result.statusCode).to.equal(400);
done();
});
});
@@ -421,6 +404,16 @@ describe('Users API', function () {
});
});
it('create user missing invite fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL_2 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('create user reserved name fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
@@ -443,9 +436,10 @@ describe('Users API', function () {
it('create second and third user', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL_2 })
.send({ username: USERNAME_2, email: EMAIL_2, invite: false })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
@@ -453,12 +447,12 @@ describe('Users API', function () {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_3, email: EMAIL_3 })
.send({ username: USERNAME_3, email: EMAIL_3, invite: true })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
// two mails for user creation
checkMails(2, done);
// one mail for first user creation, two mails for second user creation (see 'invite' flag)
checkMails(3, done);
});
});
});
@@ -502,13 +496,13 @@ describe('Users API', function () {
expect(error).to.be(null);
expect(res.statusCode).to.equal(200);
expect(res.body.users).to.be.an('array');
expect(res.body.users.length).to.equal(5);
expect(res.body.users.length).to.equal(4);
res.body.users.forEach(function (user) {
expect(user).to.be.an('object');
expect(user.id).to.be.ok();
expect(user.username).to.be.ok();
expect(user.email).to.be.ok();
if (!user.email.startsWith('unnamed')) expect(user.username).to.be.ok();
expect(user.password).to.not.be.ok();
expect(user.salt).to.not.be.ok();
expect(user.groupIds).to.not.be.ok();
@@ -682,7 +676,7 @@ describe('Users API', function () {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_4, email: EMAIL_4, password: 'tooweak' })
.send({ username: USERNAME_4, email: EMAIL_4, invite: false, password: 'tooweak' })
.end(function (error, result) {
expect(error).to.be.ok();
expect(result.statusCode).to.equal(400);
@@ -693,23 +687,23 @@ describe('Users API', function () {
it('can create user with a password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_4, email: EMAIL_4, password: 'Secret1#' })
.send({ username: USERNAME_4, email: EMAIL_4, invite: false, password: 'Secret1#' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
user_4 = result.body;
userToken = tokendb.generateToken();
token = tokendb.generateToken();
var expires = Date.now() + 2000; // 1 sec
tokendb.add(userToken, user_4.id, null, expires, accesscontrol.SCOPE_PROFILE, '', done);
tokendb.add(token, user_4.id, null, expires, accesscontrol.SCOPE_PROFILE, done);
});
});
it('can get profile of user with pre-set password', function (done) {
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: userToken })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -718,42 +712,5 @@ describe('Users API', function () {
done();
});
});
// Change password
it('change password fails due to missing token', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password')
.send({ password: 'youdontsay' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('change password fails due to small password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password')
.query({ access_token: token })
.send({ password: 'small' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('change password succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password')
.query({ access_token: token })
.send({ password: 'bigenough' })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('did change the user password', function (done) {
users.verify(user_0.id, 'bigenough', function (error) {
expect(error).to.be(null);
done();
});
});
});
+6 -34
View File
@@ -6,9 +6,7 @@ exports = module.exports = {
list: list,
create: create,
remove: remove,
changePassword: changePassword,
verifyPassword: verifyPassword,
createInvite: createInvite,
sendInvite: sendInvite,
setGroups: setGroups,
transferOwnership: transferOwnership
@@ -29,17 +27,18 @@ function create(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if (typeof req.body.invite !== 'boolean') return next(new HttpError(400, 'invite must be boolean'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if ('admin' in req.body && typeof req.body.admin !== 'boolean') return next(new HttpError(400, 'admin flag must be a boolean'));
var password = req.body.password || null;
var email = req.body.email;
var sendInvite = req.body.invite;
var username = 'username' in req.body ? req.body.username : null;
var displayName = req.body.displayName || '';
users.create(username, password, email, displayName, { invitor: req.user, admin: req.body.admin }, auditSource(req), function (error, user) {
users.create(username, password, email, displayName, { invitor: req.user, sendInvite: sendInvite }, auditSource(req), function (error, user) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
@@ -138,26 +137,14 @@ function verifyPassword(req, res, next) {
});
}
function createInvite(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
users.createInvite(req.params.userId, function (error, resetToken) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { resetToken: resetToken }));
});
}
function sendInvite(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
users.sendInvite(req.params.userId, { invitor: req.user }, function (error) {
users.sendInvite(req.params.userId, { invitor: req.user }, function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(409, 'Call createInvite API first'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { }));
next(new HttpSuccess(200, { resetToken: result }));
});
}
@@ -187,19 +174,4 @@ function transferOwnership(req, res, next) {
next(new HttpSuccess(200, {}));
});
}
function changePassword(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.userId, 'string');
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
users.setPassword(req.params.userId, req.body.password, function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
}
+1 -7
View File
@@ -32,13 +32,7 @@ if [[ "${BOX_ENV}" == "cloudron" ]]; then
if [[ "${cmd}" == "remove" ]]; then
echo "Removing collectd stats of ${appid}"
for i in {1..10}; do
if rm -rf ${HOME}/platformdata/graphite/whisper/collectd/localhost/*${appid}*; then
break
fi
echo "Failed to remove collectd directory. collectd possibly generated data in the middle of removal"
sleep 3
done
rm -rf ${HOME}/platformdata/graphite/whisper/collectd/localhost/*${appid}*
fi
fi
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
if [[ "${BOX_ENV}" == "cloudron" ]]; then
readonly app_data_dir="${HOME}/appsdata/$1"
mkdir -p "${app_data_dir}/data"
# only the top level ownership is changed because containers own the subdirectores
# and will chown them as necessary
chown yellowtent:yellowtent "${app_data_dir}"
chown yellowtent:yellowtent "${app_data_dir}/data"
else
readonly app_data_dir="${HOME}/.cloudron_test/appsdata/$1"
mkdir -p "${app_data_dir}/data"
chown ${SUDO_USER}:${SUDO_USER} "${app_data_dir}"
fi
-39
View File
@@ -1,39 +0,0 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
addon="$1"
appid="${2:-}" # only valid for redis
if [[ "${addon}" != "postgresql" && "${addon}" != "mysql" && "${addon}" != "mongodb" && "${addon}" != "redis" ]]; then
echo "${addon} must be postgresql/mysql/mongodb/redis"
exit 1
fi
if [[ "${BOX_ENV}" == "cloudron" ]]; then
readonly addon_dir="${HOME}/platformdata/${addon}/${appid}"
else
readonly addon_dir="${HOME}/.cloudron_test/platformdata/${addon}/${appid}"
fi
rm -rf "${addon_dir}"
if [[ "${addon}" != "redis" ]]; then
mkdir "${addon_dir}"
elif [[ "${BOX_ENV}" == "cloudron" ]]; then
rm -rf "${HOME}/appsdata/${appid}/redis" # legacy directory
fi
@@ -20,13 +20,18 @@ fi
# this script is called from redis addon as well!
appid="$1"
subdir="$2"
rmdir="$2"
if [[ "${BOX_ENV}" == "cloudron" ]]; then
readonly volume_dir="${HOME}/appsdata/${appid}/${subdir}"
readonly app_data_dir="${HOME}/appsdata/${appid}"
else
readonly volume_dir="${HOME}/.cloudron_test/appsdata/${appid}/${subdir}"
readonly app_data_dir="${HOME}/.cloudron_test/appsdata/${appid}"
fi
rm -rf "${volume_dir}"
# the approach below ensures symlinked contents are also deleted
find -H "${app_data_dir}" -mindepth 1 -delete || true # -H means resolve symlink in args
if [[ "${rmdir}" == "true" ]]; then
rm -rf "${app_data_dir}"
fi
+8 -3
View File
@@ -8,18 +8,20 @@ if [[ ${EUID} -ne 0 ]]; then
fi
readonly UPDATER_SERVICE="cloudron-updater"
readonly DATA_FILE="/root/cloudron-update-data.json"
if [[ $# == 1 && "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
if [[ $# != 1 ]]; then
echo "sourceDir argument required"
if [[ $# != 2 ]]; then
echo "sourceDir and data arguments required"
exit 1
fi
readonly source_dir="${1}"
readonly data="${2}"
echo "Updating Cloudron with ${source_dir}"
@@ -30,8 +32,11 @@ if systemctl reset-failed "${UPDATER_SERVICE}"; then
echo "=> service has failed earlier"
fi
# Save user data in file, to avoid argument length limit with systemd-run
echo "${data}" > "${DATA_FILE}"
echo "=> Run installer.sh as cloudron-updater.service"
if ! systemd-run --unit "${UPDATER_SERVICE}" ${installer_path}; then
if ! systemd-run --unit "${UPDATER_SERVICE}" ${installer_path} --data-file "${DATA_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
+24 -31
View File
@@ -19,6 +19,7 @@ var accesscontrol = require('./accesscontrol.js'),
passport = require('passport'),
path = require('path'),
routes = require('./routes/index.js'),
setup = require('./setup.js'),
ws = require('ws');
var gHttpServer = null;
@@ -93,7 +94,7 @@ function initializeExpressSync() {
var usersReadScope = routes.accesscontrol.scope(accesscontrol.SCOPE_USERS_READ);
var usersManageScope = routes.accesscontrol.scope(accesscontrol.SCOPE_USERS_MANAGE);
var appsReadScope = routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_READ);
var appsManageScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_MANAGE), routes.apps.verifyOwnership ];
var appsManageScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_MANAGE), routes.accesscontrol.verifyAppOwnership ];
var settingsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_SETTINGS);
var mailScope = routes.accesscontrol.scope(accesscontrol.SCOPE_MAIL);
var clientsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_CLIENTS);
@@ -101,17 +102,14 @@ function initializeExpressSync() {
var domainsManageScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_MANAGE);
var appstoreScope = routes.accesscontrol.scope(accesscontrol.SCOPE_APPSTORE);
const isUnmanaged = routes.accesscontrol.isUnmanaged;
const verifyDomainLock = routes.domains.verifyDomainLock;
// csrf protection
var csrf = routes.oauth2.csrf();
// public routes
router.post('/api/v1/cloudron/setup', routes.setup.providerTokenAuth, routes.setup.provision); // only available until no-domain
router.post('/api/v1/cloudron/dns_setup', routes.setup.providerTokenAuth, routes.setup.dnsSetup); // only available until no-domain
router.post('/api/v1/cloudron/restore', routes.setup.restore); // only available until activated
router.post('/api/v1/cloudron/activate', routes.setup.setupTokenAuth, routes.setup.activate);
router.get ('/api/v1/cloudron/status', routes.cloudron.getStatus);
router.get ('/api/v1/cloudron/status', routes.setup.getStatus);
router.get ('/api/v1/cloudron/progress', routes.cloudron.getProgress);
router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar
@@ -128,10 +126,10 @@ function initializeExpressSync() {
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.cloudron.getDisks);
router.get ('/api/v1/cloudron/logs/:unit', cloudronScope, routes.cloudron.getLogs);
router.get ('/api/v1/cloudron/logstream/:unit', cloudronScope, routes.cloudron.getLogStream);
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, isUnmanaged, routes.ssh.getAuthorizedKeys);
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, isUnmanaged, routes.ssh.addAuthorizedKey);
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.getAuthorizedKey);
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.delAuthorizedKey);
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.getAuthorizedKeys);
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.addAuthorizedKey);
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.ssh.getAuthorizedKey);
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.ssh.delAuthorizedKey);
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.get);
// config route (for dashboard)
@@ -151,10 +149,8 @@ function initializeExpressSync() {
router.get ('/api/v1/users/:userId', usersManageScope, routes.users.get); // this is manage scope because it returns non-restricted fields
router.del ('/api/v1/users/:userId', usersManageScope, routes.users.verifyPassword, routes.users.remove);
router.post('/api/v1/users/:userId', usersManageScope, routes.users.update);
router.post('/api/v1/users/:userId/password', usersManageScope, routes.users.changePassword);
router.put ('/api/v1/users/:userId/groups', usersManageScope, routes.users.setGroups);
router.post('/api/v1/users/:userId/send_invite', usersManageScope, routes.users.sendInvite);
router.post('/api/v1/users/:userId/create_invite', usersManageScope, routes.users.createInvite);
router.post('/api/v1/users/:userId/invite', usersManageScope, routes.users.sendInvite);
router.post('/api/v1/users/:userId/transfer', usersManageScope, routes.users.transferOwnership);
// Group management
@@ -211,7 +207,7 @@ function initializeExpressSync() {
router.get ('/api/v1/apps/:id/logs', appsManageScope, routes.apps.getLogs);
router.get ('/api/v1/apps/:id/exec', appsManageScope, routes.apps.exec);
// websocket cannot do bearer authentication
router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, [ accesscontrol.SCOPE_APPS_MANAGE ]), routes.apps.verifyOwnership, routes.apps.execWebSocket);
router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, [ accesscontrol.SCOPE_APPS_MANAGE ]), routes.accesscontrol.verifyAppOwnership, routes.apps.execWebSocket);
router.post('/api/v1/apps/:id/clone', appsManageScope, routes.apps.cloneApp);
router.get ('/api/v1/apps/:id/download', appsManageScope, routes.apps.downloadFile);
router.post('/api/v1/apps/:id/upload', appsManageScope, multipart, routes.apps.uploadFile);
@@ -226,19 +222,17 @@ function initializeExpressSync() {
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.settings.setCloudronName);
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.settings.getCloudronAvatar);
router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar);
router.get ('/api/v1/settings/backup_config', settingsScope, isUnmanaged, routes.settings.getBackupConfig);
router.post('/api/v1/settings/backup_config', settingsScope, isUnmanaged, routes.settings.setBackupConfig);
router.get ('/api/v1/settings/platform_config', settingsScope, isUnmanaged, routes.settings.getPlatformConfig);
router.post('/api/v1/settings/platform_config', settingsScope, isUnmanaged, routes.settings.setPlatformConfig);
router.get ('/api/v1/settings/dynamic_dns', settingsScope, isUnmanaged, routes.settings.getDynamicDnsConfig);
router.post('/api/v1/settings/dynamic_dns', settingsScope, isUnmanaged, routes.settings.setDynamicDnsConfig);
router.get ('/api/v1/settings/backup_config', settingsScope, routes.settings.getBackupConfig);
router.post('/api/v1/settings/backup_config', settingsScope, routes.settings.setBackupConfig);
router.get ('/api/v1/settings/platform_config', settingsScope, routes.settings.getPlatformConfig);
router.post('/api/v1/settings/platform_config', settingsScope, routes.settings.setPlatformConfig);
router.get ('/api/v1/settings/spaces_config', settingsScope, routes.settings.getSpacesConfig);
router.post('/api/v1/settings/spaces_config', settingsScope, routes.settings.setSpacesConfig);
router.get ('/api/v1/settings/time_zone', settingsScope, routes.settings.getTimeZone);
router.post('/api/v1/settings/time_zone', settingsScope, routes.settings.setTimeZone);
router.get ('/api/v1/settings/appstore_config', appstoreScope, isUnmanaged, routes.settings.getAppstoreConfig);
router.post('/api/v1/settings/appstore_config', appstoreScope, isUnmanaged, routes.settings.setAppstoreConfig);
router.post('/api/v1/settings/registry_config', appstoreScope, routes.settings.setRegistryConfig);
router.get ('/api/v1/settings/appstore_config', appstoreScope, routes.settings.getAppstoreConfig);
router.post('/api/v1/settings/appstore_config', appstoreScope, routes.settings.setAppstoreConfig);
// email routes
router.get ('/api/v1/mail/:domain', mailScope, routes.mail.getDomain);
@@ -267,7 +261,7 @@ function initializeExpressSync() {
router.del ('/api/v1/mail/:domain/lists/:name', mailScope, routes.mail.removeList);
// feedback
router.post('/api/v1/feedback', cloudronScope, isUnmanaged, routes.cloudron.feedback);
router.post('/api/v1/feedback', cloudronScope, routes.cloudron.feedback);
// backup routes
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
@@ -276,10 +270,9 @@ function initializeExpressSync() {
// domain routes
router.post('/api/v1/domains', domainsManageScope, routes.domains.add);
router.get ('/api/v1/domains', domainsReadScope, routes.domains.getAll);
router.get ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.get); // this is manage scope because it returns non-restricted fields
router.put ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.update);
router.post ('/api/v1/domains/:domain/renew_certs', domainsManageScope, verifyDomainLock, routes.domains.renewCerts);
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.users.verifyPassword, routes.domains.del);
router.get ('/api/v1/domains/:domain', domainsManageScope, routes.domains.get); // this is manage scope because it returns non-restricted fields
router.put ('/api/v1/domains/:domain', domainsManageScope, routes.domains.update);
router.del ('/api/v1/domains/:domain', domainsManageScope, routes.users.verifyPassword, routes.domains.del);
// caas routes
router.get('/api/v1/caas/config', cloudronScope, routes.caas.getConfig);
@@ -343,7 +336,6 @@ function initializeSysadminExpressSync() {
router.post('/api/v1/backup', routes.sysadmin.backup);
router.post('/api/v1/update', routes.sysadmin.update);
router.post('/api/v1/retire', routes.sysadmin.retire);
router.post('/api/v1/apps/:id/import', routes.sysadmin.importAppDatabase);
return httpServer;
}
@@ -361,6 +353,7 @@ function start(callback) {
routes.accesscontrol.initialize, // hooks up authentication strategies into passport
database.initialize,
cloudron.initialize,
setup.configureWebadmin,
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
gSysadminHttpServer.listen.bind(gSysadminHttpServer, config.get('sysadminPort'), '127.0.0.1'),
eventlog.add.bind(null, eventlog.ACTION_START, { userId: null, username: 'boot' }, { version: config.version() })
@@ -379,7 +372,7 @@ function stop(callback) {
gHttpServer.close.bind(gHttpServer),
gSysadminHttpServer.close.bind(gSysadminHttpServer)
], function (error) {
if (error) return callback(error);
if (error) console.error(error);
routes.oauth2.uninitialize();

Some files were not shown because too many files have changed in this diff Show More