Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58072892d6 | |||
| 85a897c78c | |||
| 6adf5772d8 | |||
| f98e3b1960 | |||
| 671a967e35 | |||
| 950ef0074f | |||
| 5515324fd4 | |||
| e72622ed4f | |||
| e821733a58 | |||
| a03c0e4475 | |||
| 3203821546 | |||
| 16f3cee5c5 | |||
| 57afb46cbd | |||
| 91dde5147a | |||
| d0692f7379 | |||
| e360658c6e | |||
| e7dc77e6de | |||
| e240a8b58f | |||
| 38d4f2c27b | |||
| 552e2a036c | |||
| 2d4b978032 | |||
| 36e00f0c84 | |||
| ef64b2b945 | |||
| f6cd33ae24 | |||
| dd109f149f | |||
| 5b62d63463 | |||
| 3fec599c0c | |||
| e30ea9f143 | |||
| 7cb0c31c59 | |||
| b00a7e3cbb | |||
| e63446ffa2 | |||
| 580da19bc2 | |||
| 936f456cec | |||
| 5d6a02f73c | |||
| b345195ea9 | |||
| 3e6b66751c | |||
| f78571e46d | |||
| f52000958c | |||
| 5ac9c6ce02 | |||
| 1110a67483 | |||
| 57bb1280f8 | |||
| 25c000599f | |||
| 86f45e2769 | |||
| 7110240e73 | |||
| 1da37b66d8 | |||
| f1975d8f2b | |||
| f407ce734a | |||
| f813cfa8db | |||
| d5880cb953 | |||
| 95da9744c1 | |||
| 85c3e45cde | |||
| 520a396ded | |||
| 13ad611c96 | |||
| 85f58d9681 | |||
| c1de62acef | |||
| 7e47e36773 | |||
| 00b6217cab | |||
| acc2b5a1a3 | |||
| b06feaa36b | |||
| 89cf8a455a | |||
| 710046a94f | |||
| b366b0fa6a | |||
| f9e7a8207a | |||
| 6178bf3d4b | |||
| f3b979f112 | |||
| 9faae96d61 | |||
| 2135fe5dd0 | |||
| 007a8d248d | |||
| 58d4a3455b | |||
| 8e3c14f245 | |||
| 91af2495a6 | |||
| 7d7df5247b | |||
| f99450d264 |
@@ -1854,3 +1854,48 @@
|
||||
* Make mail eventlog only visible to owners
|
||||
* Make app password work with sftp
|
||||
|
||||
[5.1.0]
|
||||
* Add turn addon
|
||||
* Fix disk usage display
|
||||
* Drop support for TLSv1 and TLSv1.1
|
||||
* Make cert validation work for ECC certs
|
||||
* Add type filter to mail eventlog
|
||||
* mail: Fix listing of mailboxes and aliases in the UI
|
||||
* branding: fix login page title
|
||||
* Only a Cloudron owner can install/update/exec apps with the docker addon
|
||||
* security: reset tokens are only valid for a day
|
||||
* mail: fix eventlog db perms
|
||||
* Fix various bugs in the disk graphs
|
||||
|
||||
[5.1.1]
|
||||
* Add turn addon
|
||||
* Fix disk usage display
|
||||
* Drop support for TLSv1 and TLSv1.1
|
||||
* Make cert validation work for ECC certs
|
||||
* Add type filter to mail eventlog
|
||||
* mail: Fix listing of mailboxes and aliases in the UI
|
||||
* branding: fix login page title
|
||||
* Only a Cloudron owner can install/update/exec apps with the docker addon
|
||||
* security: reset tokens are only valid for a day
|
||||
* mail: fix eventlog db perms
|
||||
* Fix various bugs in the disk graphs
|
||||
* Fix collectd installation
|
||||
* graphs: sort disk contents by usage
|
||||
* backups: show apps that are not automatically backed up in backup view
|
||||
|
||||
[5.1.2]
|
||||
* Add turn addon
|
||||
* Fix disk usage display
|
||||
* Drop support for TLSv1 and TLSv1.1
|
||||
* Make cert validation work for ECC certs
|
||||
* Add type filter to mail eventlog
|
||||
* mail: Fix listing of mailboxes and aliases in the UI
|
||||
* branding: fix login page title
|
||||
* Only a Cloudron owner can install/update/exec apps with the docker addon
|
||||
* security: reset tokens are only valid for a day
|
||||
* mail: fix eventlog db perms
|
||||
* Fix various bugs in the disk graphs
|
||||
* Fix collectd installation
|
||||
* graphs: sort disk contents by usage
|
||||
* backups: show apps that are not automatically backed up in backup view
|
||||
* turn: deny local address peers https://www.rtcsec.com/2020/04/01-slack-webrtc-turn-compromise/
|
||||
|
||||
@@ -44,7 +44,6 @@ apt-get -y install \
|
||||
linux-generic \
|
||||
logrotate \
|
||||
mysql-server-5.7 \
|
||||
nginx-full \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
resolvconf \
|
||||
@@ -54,6 +53,17 @@ apt-get -y install \
|
||||
unbound \
|
||||
xfsprogs
|
||||
|
||||
if [[ "${ubuntu_version}" == "16.04" ]]; then
|
||||
echo "==> installing nginx for xenial for TLSv3 support"
|
||||
|
||||
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/nginx.deb
|
||||
rm /tmp/nginx.deb
|
||||
else
|
||||
apt install -y nginx-full
|
||||
fi
|
||||
|
||||
# on some providers like scaleway the sudo file is changed and we want to keep the old one
|
||||
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
|
||||
|
||||
@@ -111,7 +121,7 @@ for image in ${images}; do
|
||||
done
|
||||
|
||||
echo "==> Install collectd"
|
||||
if ! apt-get install -y collectd collectd-utils; then
|
||||
if ! apt-get install -y libcurl3-gnutls collectd collectd-utils; then
|
||||
# FQDNLookup is true in default debian config. The box code has a custom collectd.conf that fixes this
|
||||
echo "Failed to install collectd. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
|
||||
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN resetTokenCreationTime', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
let async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps MODIFY mailboxDomain VARCHAR(128)', [], function (error) { // make it nullable
|
||||
if (error) console.error(error);
|
||||
|
||||
// clear mailboxName/Domain for apps that do not use mail addons
|
||||
db.all('SELECT * FROM apps', function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
var manifest = JSON.parse(app.manifestJson);
|
||||
if (manifest.addons['sendmail'] || manifest.addons['recvmail']) return iteratorDone();
|
||||
|
||||
db.runSql('UPDATE apps SET mailboxName=?, mailboxDomain=? WHERE id=?', [ null, null, app.id ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps MODIFY manifestJson VARCHAR(128) NOT NULL', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -28,6 +28,9 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
|
||||
source VARCHAR(128) DEFAULT "",
|
||||
role VARCHAR(32),
|
||||
resetToken VARCHAR(128) DEFAULT "",
|
||||
resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
active BOOLEAN DEFAULT 1,
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
@@ -76,8 +79,8 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
reverseProxyConfigJson TEXT, // { robotsTxt, csp }
|
||||
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
|
||||
enableAutomaticUpdate BOOLEAN DEFAULT 1,
|
||||
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
|
||||
mailboxDomain VARCHAR(128) NOT NULL, // mailbox domain of this apps
|
||||
mailboxName VARCHAR(128), // mailbox of this app
|
||||
mailboxDomain VARCHAR(128), // mailbox domain of this apps
|
||||
label VARCHAR(128), // display name
|
||||
tagsJson VARCHAR(2048), // array of tags
|
||||
dataDir VARCHAR(256) UNIQUE,
|
||||
|
||||
Generated
+11
-11
@@ -736,22 +736,22 @@
|
||||
}
|
||||
},
|
||||
"cloudron-manifestformat": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-4.0.0.tgz",
|
||||
"integrity": "sha512-St/Quu8ofQOf0rUAMaIsOL0u0dZ46irweU8rYVMvAXU0CGwSD9KDaeLW5NjGRg3FVjNzladUDVUE/BGD4rwEvA==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.1.1.tgz",
|
||||
"integrity": "sha512-1mArahTp9qkYRQsUJfpT/x6est1qW+gKPF+HoFU0hPuOVuBdMkfu6UUmwZDYmQF4FrbQkir46GyQAJADaXBg6g==",
|
||||
"requires": {
|
||||
"cron": "^1.7.2",
|
||||
"cron": "^1.8.2",
|
||||
"java-packagename-regex": "^1.0.0",
|
||||
"safetydance": "0.7.1",
|
||||
"semver": "^6.3.0",
|
||||
"safetydance": "1.0.0",
|
||||
"semver": "^7.1.3",
|
||||
"tv4": "^1.3.0",
|
||||
"validator": "^12.0.0"
|
||||
"validator": "^12.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"safetydance": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.7.1.tgz",
|
||||
"integrity": "sha1-FOtNQqHKr8UUVVK2zmnJuIJN0qo="
|
||||
"semver": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz",
|
||||
"integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA=="
|
||||
},
|
||||
"validator": {
|
||||
"version": "12.2.0",
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
"async": "^2.6.3",
|
||||
"aws-sdk": "^2.610.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"cloudron-manifestformat": "^4.0.0",
|
||||
"cloudron-manifestformat": "^5.1.1",
|
||||
"connect": "^3.7.0",
|
||||
"connect-lastmile": "^1.2.2",
|
||||
"connect-timeout": "^1.9.0",
|
||||
|
||||
@@ -13,7 +13,7 @@ HELP_MESSAGE="
|
||||
This script collects diagnostic information to help debug server related issues
|
||||
|
||||
Options:
|
||||
--admin-login Login as administrator
|
||||
--owner-login Login as owner
|
||||
--enable-ssh Enable SSH access for the Cloudron support team
|
||||
--help Show this message
|
||||
"
|
||||
@@ -26,7 +26,7 @@ fi
|
||||
|
||||
enableSSH="false"
|
||||
|
||||
args=$(getopt -o "" -l "help,enable-ssh,admin-login" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
@@ -34,6 +34,9 @@ while true; do
|
||||
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
|
||||
--enable-ssh) enableSSH="true"; shift;;
|
||||
--admin-login)
|
||||
# fall through
|
||||
;&
|
||||
--owner-login)
|
||||
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' LIMIT 1" 2>/dev/null)
|
||||
admin_password=$(pwgen -1s 12)
|
||||
ghost_file=/home/yellowtent/platformdata/cloudron_ghost.json
|
||||
|
||||
@@ -56,6 +56,15 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
|
||||
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
fi
|
||||
|
||||
readonly nginx_version=$(nginx -v)
|
||||
if [[ "${nginx_version}" != *"1.14."* && "${ubuntu_version}" == "16.04" ]]; then
|
||||
echo "==> installer: installing nginx for xenial for TLSv3 support"
|
||||
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
|
||||
rm /tmp/nginx.deb
|
||||
fi
|
||||
|
||||
echo "==> installer: updating node"
|
||||
if [[ "$(node --version)" != "v10.18.1" ]]; then
|
||||
mkdir -p /usr/local/node-10.18.1
|
||||
|
||||
@@ -12,6 +12,11 @@ iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
# ssh is allowed alternately on port 202
|
||||
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT
|
||||
|
||||
# turn and stun service
|
||||
iptables -t filter -A CLOUDRON -p tcp -m multiport --dports 3478,5349 -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 3478,5349 -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 50000:51000 -j ACCEPT
|
||||
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT
|
||||
|
||||
@@ -6,7 +6,8 @@ PATHS = [] # { name, dir, exclude }
|
||||
INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
|
||||
|
||||
def du(pathinfo):
|
||||
cmd = 'timeout 1800 du -Dsb "{}"'.format(pathinfo['dir'])
|
||||
# -B1 makes du print block sizes and not apparent sizes (to match df which also uses block sizes)
|
||||
cmd = 'timeout 1800 du -DsB1 "{}"'.format(pathinfo['dir'])
|
||||
if pathinfo['exclude'] != '':
|
||||
cmd += ' --exclude "{}"'.format(pathinfo['exclude'])
|
||||
|
||||
@@ -26,6 +27,7 @@ def parseSize(size):
|
||||
|
||||
def dockerSize():
|
||||
# use --format '{{json .}}' to dump the string. '{{if eq .Type "Images"}}{{.Size}}{{end}}' still creates newlines
|
||||
# https://godoc.org/github.com/docker/go-units#HumanSize is used. so it's 1000 (KB) and not 1024 (KiB)
|
||||
cmd = 'timeout 1800 docker system df --format "{{.Size}}" | head -n1'
|
||||
try:
|
||||
size = subprocess.check_output(cmd, shell=True).strip().decode('utf-8')
|
||||
|
||||
+91
-22
@@ -22,10 +22,6 @@ exports = module.exports = {
|
||||
|
||||
getServiceDetails: getServiceDetails,
|
||||
|
||||
// exported for testing
|
||||
_setupOauth: setupOauth,
|
||||
_teardownOauth: teardownOauth,
|
||||
|
||||
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
|
||||
SERVICE_STATUS_ACTIVE: 'active',
|
||||
SERVICE_STATUS_STOPPED: 'stopped'
|
||||
@@ -67,6 +63,13 @@ const RMADDONDIR_CMD = path.join(__dirname, 'scripts/rmaddondir.sh');
|
||||
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
|
||||
// teardown is destructive. app data stored with the addon is lost
|
||||
var KNOWN_ADDONS = {
|
||||
turn: {
|
||||
setup: setupTurn,
|
||||
teardown: teardownTurn,
|
||||
backup: NOOP,
|
||||
restore: NOOP,
|
||||
clear: NOOP
|
||||
},
|
||||
email: {
|
||||
setup: setupEmail,
|
||||
teardown: teardownEmail,
|
||||
@@ -102,13 +105,6 @@ var KNOWN_ADDONS = {
|
||||
restore: restoreMySql,
|
||||
clear: clearMySql,
|
||||
},
|
||||
oauth: {
|
||||
setup: setupOauth,
|
||||
teardown: teardownOauth,
|
||||
backup: NOOP,
|
||||
restore: setupOauth,
|
||||
clear: NOOP,
|
||||
},
|
||||
postgresql: {
|
||||
setup: setupPostgreSql,
|
||||
teardown: teardownPostgreSql,
|
||||
@@ -154,6 +150,11 @@ var KNOWN_ADDONS = {
|
||||
};
|
||||
|
||||
const KNOWN_SERVICES = {
|
||||
turn: {
|
||||
status: statusTurn,
|
||||
restart: restartContainer.bind(null, 'turn'),
|
||||
defaultMemoryLimit: 256 * 1024 * 1024
|
||||
},
|
||||
mail: {
|
||||
status: containerStatus.bind(null, 'mail', 'CLOUDRON_MAIL_TOKEN'),
|
||||
restart: mail.restartMail,
|
||||
@@ -237,6 +238,7 @@ function rebuildService(serviceName, callback) {
|
||||
|
||||
// this attempts to recreate the service docker container if they don't exist but platform infra version is unchanged
|
||||
// passing an infra version of 'none' will not attempt to purge existing data, not sure if this is good or bad
|
||||
if (serviceName === 'turn') return startTurn({ version: 'none' }, callback);
|
||||
if (serviceName === 'mongodb') return startMongodb({ version: 'none' }, callback);
|
||||
if (serviceName === 'postgresql') return startPostgresql({ version: 'none' }, callback);
|
||||
if (serviceName === 'mysql') return startMysql({ version: 'none' }, callback);
|
||||
@@ -650,6 +652,7 @@ function startServices(existingInfra, callback) {
|
||||
if (existingInfra.version !== infra.version) {
|
||||
debug(`startServices: ${existingInfra.version} -> ${infra.version}. starting all services`);
|
||||
startFuncs.push(
|
||||
startTurn.bind(null, existingInfra),
|
||||
startMysql.bind(null, existingInfra),
|
||||
startPostgresql.bind(null, existingInfra),
|
||||
startMongodb.bind(null, existingInfra),
|
||||
@@ -658,6 +661,7 @@ function startServices(existingInfra, callback) {
|
||||
} else {
|
||||
assert.strictEqual(typeof existingInfra.images, 'object');
|
||||
|
||||
if (!existingInfra.images.turn || infra.images.turn.tag !== existingInfra.images.turn.tag) startFuncs.push(startTurn.bind(null, existingInfra));
|
||||
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql.bind(null, existingInfra));
|
||||
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql.bind(null, existingInfra));
|
||||
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb.bind(null, existingInfra));
|
||||
@@ -677,7 +681,7 @@ function getEnvironment(app, callback) {
|
||||
appdb.getAddonConfigByAppId(app.id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` });
|
||||
if (app.manifest.addons['docker']) result.push({ name: 'CLOUDRON_DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` });
|
||||
|
||||
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
|
||||
});
|
||||
@@ -768,30 +772,37 @@ function teardownLocalStorage(app, options, callback) {
|
||||
], callback);
|
||||
}
|
||||
|
||||
function setupOauth(app, options, callback) {
|
||||
function setupTurn(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'setupOauth');
|
||||
var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8');
|
||||
if (!turnSecret) console.error('No turn secret set. Will leave emtpy, but this is a problem!');
|
||||
|
||||
if (!app.sso) return callback(null);
|
||||
const env = [
|
||||
{ name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() },
|
||||
{ name: 'CLOUDRON_STUN_PORT', value: '3478' },
|
||||
{ name: 'CLOUDRON_STUN_TLS_PORT', value: '5349' },
|
||||
{ name: 'CLOUDRON_TURN_SERVER', value: settings.adminFqdn() },
|
||||
{ name: 'CLOUDRON_TURN_PORT', value: '3478' },
|
||||
{ name: 'CLOUDRON_TURN_TLS_PORT', value: '5349' },
|
||||
{ name: 'CLOUDRON_TURN_SECRET', value: turnSecret }
|
||||
];
|
||||
|
||||
const env = [];
|
||||
debugApp(app, 'Setting up TURN');
|
||||
|
||||
debugApp(app, 'Setting oauth addon config to %j', env);
|
||||
|
||||
appdb.setAddonConfig(app.id, 'oauth', env, callback);
|
||||
appdb.setAddonConfig(app.id, 'turn', env, callback);
|
||||
}
|
||||
|
||||
function teardownOauth(app, options, callback) {
|
||||
function teardownTurn(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'teardownOauth');
|
||||
debugApp(app, 'Tearing down TURN');
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'oauth', callback);
|
||||
appdb.unsetAddonConfig(app.id, 'turn', callback);
|
||||
}
|
||||
|
||||
function setupEmail(app, options, callback) {
|
||||
@@ -1347,6 +1358,43 @@ function restorePostgreSql(app, options, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function startTurn(existingInfra, callback) {
|
||||
assert.strictEqual(typeof existingInfra, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// get and ensure we have a turn secret
|
||||
var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8');
|
||||
if (!turnSecret) {
|
||||
turnSecret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter
|
||||
safe.fs.writeFileSync(paths.ADDON_TURN_SECRET_FILE, turnSecret, 'utf8');
|
||||
}
|
||||
|
||||
const tag = infra.images.turn.tag;
|
||||
const memoryLimit = 256;
|
||||
const realm = settings.adminFqdn();
|
||||
|
||||
if (existingInfra.version === infra.version && existingInfra.images.turn && infra.images.turn.tag === existingInfra.images.turn.tag) return callback();
|
||||
|
||||
// this exports 3478/tcp, 5349/tls and 50000-51000/udp
|
||||
const cmd = `docker run --restart=always -d --name="turn" \
|
||||
--hostname turn \
|
||||
--net host \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=turn \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_TURN_SECRET="${turnSecret}" \
|
||||
-e CLOUDRON_REALM="${realm}" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.exec('startTurn', cmd, callback);
|
||||
}
|
||||
|
||||
function startMongodb(existingInfra, callback) {
|
||||
assert.strictEqual(typeof existingInfra, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -1706,6 +1754,27 @@ function restoreRedis(app, options, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function statusTurn(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.inspect('turn', function (error, container) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
|
||||
if (error) return callback(error);
|
||||
|
||||
docker.memoryUsage(container.Id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var tmp = {
|
||||
status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
|
||||
memoryUsed: result.memory_stats.usage,
|
||||
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit)
|
||||
};
|
||||
|
||||
callback(null, tmp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function statusDocker(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
+771
-834
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -21,7 +21,8 @@ exports = module.exports = {
|
||||
runSystemChecks: runSystemChecks,
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
var addons = require('./addons.js'),
|
||||
apps = require('./apps.js'),
|
||||
appstore = require('./appstore.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
@@ -326,6 +327,7 @@ function setDashboardAndMailDomain(domain, auditSource, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mail.onMailFqdnChanged(NOOP_CALLBACK); // this will update dns and re-configure mail server
|
||||
addons.restartService('turn', NOOP_CALLBACK); // to update the realm variable
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
+15
-16
@@ -23,11 +23,6 @@ var apps = require('./apps.js'),
|
||||
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 (constants.TEST) {
|
||||
req.app = { id: 'testappid' };
|
||||
@@ -64,6 +59,8 @@ function attachDockerRequest(req, res, next) {
|
||||
dockerResponse.pipe(res, { end: true });
|
||||
});
|
||||
|
||||
req.dockerRequest.on('error', () => {}); // abort() throws
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -74,22 +71,21 @@ function containersCreate(req, res, next) {
|
||||
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id, isCloudronManaged: String(false) })); // 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');
|
||||
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data');
|
||||
|
||||
debug('Original volume binds:', req.body.HostConfig.Binds);
|
||||
debug('Original bind mounts:', 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}`);
|
||||
if (!bind.startsWith('/app/data/')) {
|
||||
req.dockerRequest.abort();
|
||||
return next(new HttpError(400, 'Binds must be under /app/data/'));
|
||||
}
|
||||
|
||||
binds.push(bind.replace(new RegExp('^/app/data/'), appDataDir + '/'));
|
||||
}
|
||||
|
||||
// cleanup the paths from potential double slashes
|
||||
binds = binds.map(function (bind) { return bind.replace(/\/+/g, '/'); });
|
||||
|
||||
debug('Rewritten volume binds:', binds);
|
||||
debug('Rewritten bind mounts:', binds);
|
||||
safe.set(req.body, 'HostConfig.Binds', binds);
|
||||
|
||||
let plainBody = JSON.stringify(req.body);
|
||||
@@ -117,6 +113,9 @@ function start(callback) {
|
||||
assert(gHttpServer === null, 'Already started');
|
||||
|
||||
let json = middleware.json({ strict: true });
|
||||
|
||||
// we protect container create as the app/admin can otherwise mount random paths (like the ghost file)
|
||||
// protected other paths is done by preventing install/exec access of apps using docker addon
|
||||
let router = new express.Router();
|
||||
router.post('/:version/containers/create', containersCreate);
|
||||
|
||||
@@ -137,7 +136,7 @@ function start(callback) {
|
||||
.use(middleware.lastMile());
|
||||
|
||||
gHttpServer = http.createServer(proxyServer);
|
||||
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '0.0.0.0', callback);
|
||||
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '172.18.0.1', callback);
|
||||
|
||||
// Overwrite the default 2min request timeout. This is required for large builds for example
|
||||
gHttpServer.setTimeout(60 * 60 * 1000);
|
||||
|
||||
+20
-9
@@ -51,16 +51,21 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(name, domain, callback) {
|
||||
function add(name, data, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'object');
|
||||
assert.strictEqual(typeof domain.zoneName, 'string');
|
||||
assert.strictEqual(typeof domain.provider, 'string');
|
||||
assert.strictEqual(typeof domain.config, 'object');
|
||||
assert.strictEqual(typeof domain.tlsConfig, 'object');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof data.zoneName, 'string');
|
||||
assert.strictEqual(typeof data.provider, 'string');
|
||||
assert.strictEqual(typeof data.config, 'object');
|
||||
assert.strictEqual(typeof data.tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
|
||||
let queries = [
|
||||
{ query: 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', args: [ name, data.zoneName, data.provider, JSON.stringify(data.config), JSON.stringify(data.tlsConfig) ] },
|
||||
{ query: 'INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', args: [ name, data.dkimSelector || 'cloudron' ] },
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
@@ -100,7 +105,12 @@ function del(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM domains WHERE domain=?', [ domain ], function (error, result) {
|
||||
let queries = [
|
||||
{ query: 'DELETE FROM mail WHERE domain = ?', args: [ domain ] },
|
||||
{ query: 'DELETE FROM domains WHERE domain = ?', args: [ domain ] },
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') {
|
||||
if (error.message.indexOf('apps_mailDomain_constraint') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by an app or the mailbox of an app. Check the domains of apps and the Email section of each app.'));
|
||||
if (error.message.indexOf('subdomains') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more app(s).'));
|
||||
@@ -108,8 +118,9 @@ function del(domain, callback) {
|
||||
|
||||
return callback(new BoxError(BoxError.CONFLICT, error.message));
|
||||
}
|
||||
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
|
||||
if (results[1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
+11
-2
@@ -40,6 +40,7 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:domains'),
|
||||
domaindb = require('./domaindb.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
mail = require('./mail.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
@@ -48,6 +49,8 @@ var assert = require('assert'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
// choose which subdomain backend we use for test purpose we use route53
|
||||
function api(provider) {
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
@@ -171,7 +174,7 @@ function add(domain, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof data.tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
|
||||
let { zoneName, provider, config, fallbackCertificate, tlsConfig, dkimSelector } = data;
|
||||
|
||||
if (!tld.isValid(domain)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
|
||||
if (domain.endsWith('.')) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
|
||||
@@ -194,10 +197,12 @@ function add(domain, data, auditSource, callback) {
|
||||
let error = validateTlsConfig(tlsConfig, provider);
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!dkimSelector) dkimSelector = 'cloudron-' + settings.adminDomain().replace(/\./g, '');
|
||||
|
||||
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
|
||||
domaindb.add(domain, { zoneName, provider, config: sanitizedConfig, tlsConfig, dkimSelector }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
|
||||
@@ -205,6 +210,8 @@ function add(domain, data, auditSource, callback) {
|
||||
|
||||
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
|
||||
|
||||
mail.onDomainAdded(domain, NOOP_CALLBACK);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
@@ -314,6 +321,8 @@ function del(domain, auditSource, callback) {
|
||||
|
||||
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
|
||||
|
||||
mail.onDomainRemoved(domain, NOOP_CALLBACK);
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@ exports = module.exports = {
|
||||
// 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
|
||||
'images': {
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.1.0@sha256:eee0dfd3829d563f2063084bc0d7c8802c4bdd6e233159c6226a17ff7a9a3503' },
|
||||
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.0.2@sha256:2643b73fe371154e37647957cc7103cacb34c50737f2954abd7d70f167a1f33a' },
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.2.0@sha256:440c8a9ca4d2958d51a375359f8158ef702b83395aa9ac4f450c51825ec09239' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.1.0@sha256:f2cda21bd15c21bbf44432df412525369ef831a2d53860b5c5b1675e6f384de2' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.6.5@sha256:d17cc0a3d2b6431cb683109abf40fffb91199e2af1d6d99f81d8ec3a1e1bb442' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.7.2@sha256:f20d112ff9a97e052a9187063eabbd8d484ce369114d44186e344169a1b3ef6b' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.2.0@sha256:fc9ca69d16e6ebdbd98ed53143d4a0d2212eef60cb638dc71219234e6f427a2c' },
|
||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
|
||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:1.0.0@sha256:3b70aac36700225945a4a39b5a400c28e010e980879d0dcca76e4a37b04a16ed' }
|
||||
}
|
||||
};
|
||||
|
||||
+12
-28
@@ -7,10 +7,11 @@ exports = module.exports = {
|
||||
getDomains: getDomains,
|
||||
|
||||
getDomain: getDomain,
|
||||
addDomain: addDomain,
|
||||
removeDomain: removeDomain,
|
||||
clearDomains: clearDomains,
|
||||
|
||||
onDomainAdded: onDomainAdded,
|
||||
onDomainRemoved: onDomainRemoved,
|
||||
|
||||
removePrivateFields: removePrivateFields,
|
||||
|
||||
setDnsRecords: setDnsRecords,
|
||||
@@ -312,9 +313,8 @@ 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]);
|
||||
const actual = txtToDict(dmarc.value);
|
||||
dmarc.status = actual.v === 'DMARC1'; // see box#666
|
||||
}
|
||||
|
||||
callback(null, dmarc);
|
||||
@@ -905,37 +905,21 @@ function onMailFqdnChanged(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addDomain(domain, callback) {
|
||||
function onDomainAdded(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dkimSelector = domain === settings.adminDomain() ? 'cloudron' : ('cloudron-' + settings.adminDomain().replace(/\./g, ''));
|
||||
|
||||
maildb.add(domain, { dkimSelector }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.series([
|
||||
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
|
||||
restartMailIfActivated
|
||||
], NOOP_CALLBACK); // do these asynchronously
|
||||
|
||||
callback();
|
||||
});
|
||||
async.series([
|
||||
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
|
||||
restartMailIfActivated
|
||||
], callback);
|
||||
}
|
||||
|
||||
function removeDomain(domain, callback) {
|
||||
function onDomainRemoved(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (domain === settings.adminDomain()) return callback(new BoxError(BoxError.CONFLICT));
|
||||
|
||||
maildb.del(domain, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
restartMail(NOOP_CALLBACK);
|
||||
|
||||
callback();
|
||||
});
|
||||
restartMail(callback);
|
||||
}
|
||||
|
||||
function clearDomains(callback) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
del: del,
|
||||
get: get,
|
||||
list: list,
|
||||
update: update,
|
||||
@@ -34,20 +32,6 @@ function postProcess(data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
function add(domain, data, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', [ domain, data.dkimSelector || 'cloudron' ], function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mail domain already exists'));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND), 'no such domain');
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -58,20 +42,6 @@ function clear(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function del(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// deletes aliases as well
|
||||
database.query('DELETE FROM mail WHERE domain=?', [ domain ], function (error, result) {
|
||||
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new BoxError(BoxError.CONFLICT));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mail domain not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function get(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
+7
-8
@@ -58,17 +58,16 @@ server {
|
||||
ssl_certificate <%= certFilePath %>;
|
||||
ssl_certificate_key <%= keyFilePath %>;
|
||||
ssl_session_timeout 5m;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
|
||||
ssl_session_tickets off;
|
||||
|
||||
# https://bettercrypto.org/static/applied-crypto-hardening.pdf
|
||||
# https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||
# https://cipherli.st/
|
||||
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
|
||||
# https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices#25-use-forward-secrecy
|
||||
# ciphers according to https://ssl-config.mozilla.org/#server=nginx&version=1.14.0&config=intermediate&openssl=1.1.1&guideline=5.4
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# ciphers according to https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.3&openssl=1.0.2g&hsts=yes&profile=modern
|
||||
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
|
||||
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
|
||||
add_header Strict-Transport-Security "max-age=15768000";
|
||||
|
||||
|
||||
+2
-1
@@ -22,7 +22,7 @@ exports = module.exports = {
|
||||
|
||||
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
|
||||
APPS_DATA_DIR: path.join(baseDir(), 'appsdata'),
|
||||
BOX_DATA_DIR: path.join(baseDir(), 'boxdata'),
|
||||
BOX_DATA_DIR: path.join(baseDir(), 'boxdata'), // box data dir is part of box backup
|
||||
|
||||
ACME_CHALLENGES_DIR: path.join(baseDir(), 'platformdata/acme'),
|
||||
ADDON_CONFIG_DIR: path.join(baseDir(), 'platformdata/addons'),
|
||||
@@ -46,6 +46,7 @@ exports = module.exports = {
|
||||
APP_CERTS_DIR: path.join(baseDir(), 'boxdata/certs'),
|
||||
CLOUDRON_AVATAR_FILE: path.join(baseDir(), 'boxdata/avatar.png'),
|
||||
UPDATE_CHECKER_FILE: path.join(baseDir(), 'boxdata/updatechecker.json'),
|
||||
ADDON_TURN_SECRET_FILE: path.join(baseDir(), 'boxdata/addon-turn-secret'),
|
||||
|
||||
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
|
||||
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
|
||||
|
||||
+4
-5
@@ -133,11 +133,10 @@ function pruneInfraImages(callback) {
|
||||
function stopContainers(existingInfra, callback) {
|
||||
// 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');
|
||||
async.series([
|
||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker stop'),
|
||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker rm -f')
|
||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop'),
|
||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f')
|
||||
], callback);
|
||||
} else {
|
||||
assert(typeof infra.images, 'object');
|
||||
@@ -150,8 +149,8 @@ function stopContainers(existingInfra, callback) {
|
||||
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
|
||||
// ignore error if container not found (and fail later) so that this code works across restarts
|
||||
async.series([
|
||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'network=cloudron' | xargs --no-run-if-empty docker stop || true`),
|
||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'network=cloudron' | xargs --no-run-if-empty docker rm -f || true`)
|
||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'label=isCloudronManaged' | xargs --no-run-if-empty docker stop || true`),
|
||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'label=isCloudronManaged' | xargs --no-run-if-empty docker rm -f || true`)
|
||||
], callback);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -121,7 +121,8 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
|
||||
provider: dnsConfig.provider,
|
||||
config: dnsConfig.config,
|
||||
fallbackCertificate: dnsConfig.fallbackCertificate || null,
|
||||
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
|
||||
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' },
|
||||
dkimSelector: 'cloudron'
|
||||
};
|
||||
|
||||
domains.add(domain, data, auditSource, function (error) {
|
||||
@@ -137,7 +138,6 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
|
||||
settings.setSysinfoConfig.bind(null, sysinfoConfig),
|
||||
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
|
||||
cloudron.setDashboardDomain.bind(null, domain, auditSource),
|
||||
mail.addDomain.bind(null, domain), // this relies on settings.mailFqdn() and settings.adminDomain()
|
||||
setProgress.bind(null, 'setup', 'Done'),
|
||||
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
|
||||
], function (error) {
|
||||
|
||||
+7
-7
@@ -146,19 +146,19 @@ function validateCertificate(location, domainObject, certificate) {
|
||||
// -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 });
|
||||
let result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
|
||||
if (result === null) return new BoxError(BoxError.BAD_FIELD, 'Unable to get certificate subject:' + safe.error.message, { field: 'cert' });
|
||||
|
||||
if (result.indexOf('does match certificate') === -1) return new BoxError(BoxError.BAD_FIELD, `Certificate is not valid for this domain. Expecting ${fqdn}`, { field: 'cert' });
|
||||
|
||||
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
|
||||
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
|
||||
if (certModulus === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get cert modulus: ${safe.error.message}`, { field: 'cert' });
|
||||
// check if public key in the cert and private key matches. pkey below works for RSA and ECDSA keys
|
||||
const pubKeyFromCert = safe.child_process.execSync('openssl x509 -noout -pubkey', { encoding: 'utf8', input: cert });
|
||||
if (pubKeyFromCert === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get public key from cert: ${safe.error.message}`, { field: 'cert' });
|
||||
|
||||
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
|
||||
if (keyModulus === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get key modulus: ${safe.error.message}`, { field: 'cert' });
|
||||
const pubKeyFromKey = safe.child_process.execSync('openssl pkey -pubout', { encoding: 'utf8', input: key });
|
||||
if (pubKeyFromKey === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get public key from private key: ${safe.error.message}`, { field: 'cert' });
|
||||
|
||||
if (certModulus !== keyModulus) return new BoxError(BoxError.BAD_FIELD, 'Key does not match the certificate.', { field: 'cert' });
|
||||
if (pubKeyFromCert !== pubKeyFromKey) return new BoxError(BoxError.BAD_FIELD, 'Public key does not match the certificate.', { field: 'cert' });
|
||||
|
||||
// check expiration
|
||||
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
|
||||
|
||||
+126
-95
@@ -4,16 +4,16 @@ exports = module.exports = {
|
||||
getApp: getApp,
|
||||
getApps: getApps,
|
||||
getAppIcon: getAppIcon,
|
||||
installApp: installApp,
|
||||
uninstallApp: uninstallApp,
|
||||
restoreApp: restoreApp,
|
||||
install: install,
|
||||
uninstall: uninstall,
|
||||
restore: restore,
|
||||
importApp: importApp,
|
||||
backupApp: backupApp,
|
||||
updateApp: updateApp,
|
||||
backup: backup,
|
||||
update: update,
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream,
|
||||
listBackups: listBackups,
|
||||
repairApp: repairApp,
|
||||
repair: repair,
|
||||
|
||||
setAccessRestriction: setAccessRestriction,
|
||||
setLabel: setLabel,
|
||||
@@ -31,16 +31,18 @@ exports = module.exports = {
|
||||
setLocation: setLocation,
|
||||
setDataDir: setDataDir,
|
||||
|
||||
stopApp: stopApp,
|
||||
startApp: startApp,
|
||||
restartApp: restartApp,
|
||||
stop: stop,
|
||||
start: start,
|
||||
restart: restart,
|
||||
exec: exec,
|
||||
execWebSocket: execWebSocket,
|
||||
|
||||
cloneApp: cloneApp,
|
||||
clone: clone,
|
||||
|
||||
uploadFile: uploadFile,
|
||||
downloadFile: downloadFile
|
||||
downloadFile: downloadFile,
|
||||
|
||||
load: load
|
||||
};
|
||||
|
||||
var apps = require('../apps.js'),
|
||||
@@ -51,19 +53,28 @@ var apps = require('../apps.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
safe = require('safetydance'),
|
||||
users = require('../users.js'),
|
||||
util = require('util'),
|
||||
WebSocket = require('ws');
|
||||
|
||||
function getApp(req, res, next) {
|
||||
function load(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
apps.get(req.params.id, function (error, app) {
|
||||
apps.get(req.params.id, function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, apps.removeInternalFields(app)));
|
||||
req.resource = result;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function getApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
next(new HttpSuccess(200, apps.removeInternalFields(req.resource)));
|
||||
}
|
||||
|
||||
function getApps(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
@@ -77,19 +88,19 @@ function getApps(req, res, next) {
|
||||
}
|
||||
|
||||
function getAppIcon(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
apps.getIconPath(req.params.id, { original: req.query.original }, function (error, iconPath) {
|
||||
apps.getIconPath(req.resource, { original: req.query.original }, function (error, iconPath) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
res.sendFile(iconPath);
|
||||
});
|
||||
}
|
||||
|
||||
function installApp(req, res, next) {
|
||||
function install(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
var data = req.body;
|
||||
const data = req.body;
|
||||
|
||||
// atleast one
|
||||
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
|
||||
@@ -133,20 +144,28 @@ function installApp(req, res, next) {
|
||||
|
||||
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
|
||||
|
||||
apps.install(data, req.user, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
|
||||
if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to install app with docker addon'));
|
||||
|
||||
data.appStoreId = appStoreId;
|
||||
data.manifest = manifest;
|
||||
apps.install(data, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setAccessRestriction(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
|
||||
|
||||
apps.setAccessRestriction(req.params.id, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
|
||||
apps.setAccessRestriction(req.resource, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
@@ -155,11 +174,11 @@ function setAccessRestriction(req, res, next) {
|
||||
|
||||
function setLabel(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (typeof req.body.label !== 'string') return next(new HttpError(400, 'label must be a string'));
|
||||
|
||||
apps.setLabel(req.params.id, req.body.label, auditSource.fromRequest(req), function (error) {
|
||||
apps.setLabel(req.resource, req.body.label, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
@@ -168,12 +187,12 @@ function setLabel(req, res, next) {
|
||||
|
||||
function setTags(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (!Array.isArray(req.body.tags)) return next(new HttpError(400, 'tags must be an array'));
|
||||
if (req.body.tags.some((t) => typeof t !== 'string')) return next(new HttpError(400, 'tags array must contain strings'));
|
||||
|
||||
apps.setTags(req.params.id, req.body.tags, auditSource.fromRequest(req), function (error) {
|
||||
apps.setTags(req.resource, req.body.tags, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
@@ -182,11 +201,11 @@ function setTags(req, res, next) {
|
||||
|
||||
function setIcon(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (req.body.icon !== null && typeof req.body.icon !== 'string') return next(new HttpError(400, 'icon is null or a base-64 image string'));
|
||||
|
||||
apps.setIcon(req.params.id, req.body.icon, auditSource.fromRequest(req), function (error) {
|
||||
apps.setIcon(req.resource, req.body.icon, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
@@ -195,11 +214,11 @@ function setIcon(req, res, next) {
|
||||
|
||||
function setMemoryLimit(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
||||
|
||||
apps.setMemoryLimit(req.params.id, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.setMemoryLimit(req.resource, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
@@ -208,11 +227,11 @@ function setMemoryLimit(req, res, next) {
|
||||
|
||||
function setCpuShares(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (typeof req.body.cpuShares !== 'number') return next(new HttpError(400, 'cpuShares is not a number'));
|
||||
|
||||
apps.setCpuShares(req.params.id, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.setCpuShares(req.resource, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
@@ -221,11 +240,11 @@ function setCpuShares(req, res, next) {
|
||||
|
||||
function setAutomaticBackup(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
|
||||
|
||||
apps.setAutomaticBackup(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
|
||||
apps.setAutomaticBackup(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
@@ -234,11 +253,11 @@ function setAutomaticBackup(req, res, next) {
|
||||
|
||||
function setAutomaticUpdate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
|
||||
|
||||
apps.setAutomaticUpdate(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
|
||||
apps.setAutomaticUpdate(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
@@ -247,13 +266,13 @@ function setAutomaticUpdate(req, res, next) {
|
||||
|
||||
function setReverseProxyConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (req.body.robotsTxt !== null && typeof req.body.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt is not a string'));
|
||||
|
||||
if (req.body.csp !== null && typeof req.body.csp !== 'string') return next(new HttpError(400, 'csp is not a string'));
|
||||
|
||||
apps.setReverseProxyConfig(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
|
||||
apps.setReverseProxyConfig(req.resource, req.body, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
@@ -262,14 +281,14 @@ function setReverseProxyConfig(req, res, next) {
|
||||
|
||||
function setCertificate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (req.body.key !== null && typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
if (req.body.cert !== null && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if (req.body.cert && !req.body.key) return next(new HttpError(400, 'key must be provided'));
|
||||
if (!req.body.cert && req.body.key) return next(new HttpError(400, 'cert must be provided'));
|
||||
|
||||
apps.setCertificate(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
|
||||
apps.setCertificate(req.resource, req.body, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
@@ -278,12 +297,12 @@ function setCertificate(req, res, next) {
|
||||
|
||||
function setEnvironment(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (!req.body.env || typeof req.body.env !== 'object') return next(new HttpError(400, 'env must be an object'));
|
||||
if (Object.keys(req.body.env).some((key) => typeof req.body.env[key] !== 'string')) return next(new HttpError(400, 'env must contain values as strings'));
|
||||
|
||||
apps.setEnvironment(req.params.id, req.body.env, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.setEnvironment(req.resource, req.body.env, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
@@ -292,11 +311,11 @@ function setEnvironment(req, res, next) {
|
||||
|
||||
function setDebugMode(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (req.body.debugMode !== null && typeof req.body.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
||||
|
||||
apps.setDebugMode(req.params.id, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.setDebugMode(req.resource, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
@@ -305,12 +324,12 @@ function setDebugMode(req, res, next) {
|
||||
|
||||
function setMailbox(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (req.body.mailboxName !== null && typeof req.body.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
|
||||
if (typeof req.body.mailboxDomain !== 'string') return next(new HttpError(400, 'mailboxDomain must be a string'));
|
||||
|
||||
apps.setMailbox(req.params.id, req.body.mailboxName, req.body.mailboxDomain, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.setMailbox(req.resource, req.body.mailboxName, req.body.mailboxDomain, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
@@ -319,7 +338,7 @@ function setMailbox(req, res, next) {
|
||||
|
||||
function setLocation(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (typeof req.body.location !== 'string') return next(new HttpError(400, 'location must be string')); // location may be an empty string
|
||||
if (!req.body.domain) return next(new HttpError(400, 'domain is required'));
|
||||
@@ -334,7 +353,7 @@ function setLocation(req, res, next) {
|
||||
|
||||
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
|
||||
|
||||
apps.setLocation(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.setLocation(req.resource, req.body, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
@@ -343,47 +362,49 @@ function setLocation(req, res, next) {
|
||||
|
||||
function setDataDir(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (req.body.dataDir !== null && typeof req.body.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
|
||||
|
||||
apps.setDataDir(req.params.id, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.setDataDir(req.resource, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function repairApp(req, res, next) {
|
||||
function repair(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
const data = req.body;
|
||||
|
||||
if ('manifest' in data) {
|
||||
if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
|
||||
|
||||
if (safe.query(data.manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to repair app with docker addon'));
|
||||
}
|
||||
|
||||
if ('dockerImage' in data) {
|
||||
if (!data.dockerImage || typeof data.dockerImage !== 'string') return next(new HttpError(400, 'dockerImage must be a string'));
|
||||
}
|
||||
|
||||
apps.repair(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.repair(req.resource, data, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function restoreApp(req, res, next) {
|
||||
function restore(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
var data = req.body;
|
||||
|
||||
if (!data.backupId || typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be non-empty string'));
|
||||
|
||||
apps.restore(req.params.id, data.backupId, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.restore(req.resource, data.backupId, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
@@ -392,7 +413,7 @@ function restoreApp(req, res, next) {
|
||||
|
||||
function importApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
var data = req.body;
|
||||
|
||||
@@ -414,16 +435,16 @@ function importApp(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
apps.importApp(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.importApp(req.resource, data, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function cloneApp(req, res, next) {
|
||||
function clone(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
var data = req.body;
|
||||
|
||||
@@ -434,66 +455,66 @@ function cloneApp(req, res, next) {
|
||||
|
||||
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
|
||||
|
||||
apps.clone(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.clone(req.resource, data, req.user, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(201, { id: result.id, taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function backupApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
function backup(req, res, next) {
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
apps.backup(req.params.id, function (error, result) {
|
||||
apps.backup(req.resource, function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function uninstallApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
function uninstall(req, res, next) {
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.uninstall(req.resource, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function startApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
function start(req, res, next) {
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
apps.start(req.params.id, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.start(req.resource, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function stopApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
function stop(req, res, next) {
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
apps.stop(req.params.id, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.stop(req.resource, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function restartApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
function restart(req, res, next) {
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
apps.restart(req.params.id, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.restart(req.resource, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function updateApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
function update(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
var data = req.body;
|
||||
|
||||
@@ -505,16 +526,24 @@ function updateApp(req, res, next) {
|
||||
if ('skipBackup' in data && typeof data.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean'));
|
||||
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
|
||||
|
||||
apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
|
||||
apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to update app with docker addon'));
|
||||
|
||||
data.appStoreId = appStoreId;
|
||||
data.manifest = manifest;
|
||||
apps.update(req.resource, data, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// this route is for streaming logs
|
||||
function getLogStream(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
|
||||
@@ -529,7 +558,7 @@ function getLogStream(req, res, next) {
|
||||
format: 'json'
|
||||
};
|
||||
|
||||
apps.getLogs(req.params.id, options, function (error, logStream) {
|
||||
apps.getLogs(req.resource, options, function (error, logStream) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
res.writeHead(200, {
|
||||
@@ -551,7 +580,7 @@ function getLogStream(req, res, next) {
|
||||
}
|
||||
|
||||
function getLogs(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10;
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
|
||||
@@ -562,7 +591,7 @@ function getLogs(req, res, next) {
|
||||
format: req.query.format || 'json'
|
||||
};
|
||||
|
||||
apps.getLogs(req.params.id, options, function (error, logStream) {
|
||||
apps.getLogs(req.resource, options, function (error, logStream) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
res.writeHead(200, {
|
||||
@@ -598,7 +627,7 @@ function demuxStream(stream, stdin) {
|
||||
}
|
||||
|
||||
function exec(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
var cmd = null;
|
||||
if (req.query.cmd) {
|
||||
@@ -612,9 +641,11 @@ function exec(req, res, next) {
|
||||
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
|
||||
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
|
||||
|
||||
var tty = req.query.tty === 'true' ? true : false;
|
||||
var tty = req.query.tty === 'true';
|
||||
|
||||
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
|
||||
if (safe.query(req.resource, 'manifest.addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is requied to exec app with docker addon'));
|
||||
|
||||
apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
|
||||
@@ -636,7 +667,7 @@ function exec(req, res, next) {
|
||||
}
|
||||
|
||||
function execWebSocket(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
var cmd = null;
|
||||
if (req.query.cmd) {
|
||||
@@ -652,7 +683,7 @@ function execWebSocket(req, res, next) {
|
||||
|
||||
var tty = req.query.tty === 'true' ? true : false;
|
||||
|
||||
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
|
||||
apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
req.clearTimeout();
|
||||
@@ -682,7 +713,7 @@ function execWebSocket(req, res, next) {
|
||||
}
|
||||
|
||||
function listBackups(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
|
||||
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
|
||||
@@ -690,7 +721,7 @@ function listBackups(req, res, next) {
|
||||
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
|
||||
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
|
||||
|
||||
apps.listBackups(page, perPage, req.params.id, function (error, result) {
|
||||
apps.listBackups(req.resource, page, perPage, function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, { backups: result }));
|
||||
@@ -698,12 +729,12 @@ function listBackups(req, res, next) {
|
||||
}
|
||||
|
||||
function uploadFile(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
|
||||
if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart'));
|
||||
|
||||
apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) {
|
||||
apps.uploadFile(req.resource, req.files.file.path, req.query.file, function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
@@ -711,11 +742,11 @@ function uploadFile(req, res, next) {
|
||||
}
|
||||
|
||||
function downloadFile(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
|
||||
|
||||
apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) {
|
||||
apps.downloadFile(req.resource, req.query.file, function (error, stream, info) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
var headers = {
|
||||
|
||||
@@ -102,6 +102,7 @@ function passwordReset(req, res, next) {
|
||||
users.getByResetToken(req.body.resetToken, function (error, userObject) {
|
||||
if (error) return next(new HttpError(401, 'Invalid resetToken'));
|
||||
|
||||
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
|
||||
if (!userObject.username) return next(new HttpError(409, 'No username set'));
|
||||
|
||||
// setPassword clears the resetToken
|
||||
@@ -130,6 +131,8 @@ function setupAccount(req, res, next) {
|
||||
users.getByResetToken(req.body.resetToken, function (error, userObject) {
|
||||
if (error) return next(new HttpError(401, 'Invalid Reset Token'));
|
||||
|
||||
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
|
||||
|
||||
users.update(userObject, { username: req.body.username, displayName: req.body.displayName }, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === BoxError.ALREADY_EXISTS) return next(new HttpError(409, 'Username already used'));
|
||||
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
exports = module.exports = {
|
||||
getDomain: getDomain,
|
||||
addDomain: addDomain,
|
||||
removeDomain: removeDomain,
|
||||
|
||||
setDnsRecords: setDnsRecords,
|
||||
|
||||
@@ -50,18 +48,6 @@ function getDomain(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function addDomain(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'));
|
||||
|
||||
mail.addDomain(req.body.domain, function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(201, { domain: req.body.domain }));
|
||||
});
|
||||
}
|
||||
|
||||
function setDnsRecords(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
@@ -77,16 +63,6 @@ function setDnsRecords(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeDomain(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
mail.removeDomain(req.params.domain, function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
|
||||
@@ -124,46 +124,6 @@ describe('Mail API', function () {
|
||||
after(cleanup);
|
||||
|
||||
describe('crud', function () {
|
||||
it('cannot add non-existing domain', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/mail')
|
||||
.query({ access_token: token })
|
||||
.send({ domain: 'doesnotexist.com' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('domain must be a string', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/mail')
|
||||
.query({ access_token: token })
|
||||
.send({ domain: ['doesnotexist.com'] })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can add domain', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/mail')
|
||||
.query({ access_token: token })
|
||||
.send({ domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot add domain twice', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/mail')
|
||||
.query({ access_token: token })
|
||||
.send({ domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(409);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get non-existing domain', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/mail/doesnotexist.com')
|
||||
.query({ access_token: token })
|
||||
@@ -188,33 +148,6 @@ describe('Mail API', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot delete non-existing domain', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot delete admin mail domain', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/mail/' + ADMIN_DOMAIN.domain)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(409);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can delete admin mail domain', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('status', function () {
|
||||
@@ -243,20 +176,13 @@ describe('Mail API', function () {
|
||||
mxDomain = DOMAIN_0.domain;
|
||||
dmarcDomain = '_dmarc.' + DOMAIN_0.domain;
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/mail')
|
||||
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/enable')
|
||||
.query({ access_token: token })
|
||||
.send({ domain: DOMAIN_0.domain })
|
||||
.send({ enabled: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
expect(res.statusCode).to.equal(202);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/enable')
|
||||
.query({ access_token: token })
|
||||
.send({ enabled: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -265,12 +191,7 @@ describe('Mail API', function () {
|
||||
|
||||
dns.resolve = resolve;
|
||||
|
||||
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('does not fail when dns errors', function (done) {
|
||||
@@ -503,25 +424,6 @@ describe('Mail API', function () {
|
||||
});
|
||||
|
||||
describe('mail from validation', function () {
|
||||
before(function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/mail')
|
||||
.query({ access_token: token })
|
||||
.send({ domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get mail from validation succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
|
||||
.query({ access_token: token })
|
||||
@@ -554,25 +456,6 @@ describe('Mail API', function () {
|
||||
});
|
||||
|
||||
describe('catch_all', function () {
|
||||
before(function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/mail')
|
||||
.query({ access_token: token })
|
||||
.send({ domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get catch_all succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
|
||||
.query({ access_token: token })
|
||||
@@ -624,25 +507,6 @@ describe('Mail API', function () {
|
||||
});
|
||||
|
||||
describe('mail relay', function () {
|
||||
before(function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/mail')
|
||||
.query({ access_token: token })
|
||||
.send({ domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get mail relay succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
|
||||
.query({ access_token: token })
|
||||
@@ -701,25 +565,6 @@ describe('Mail API', function () {
|
||||
});
|
||||
|
||||
describe('mailboxes', function () {
|
||||
before(function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/mail')
|
||||
.query({ access_token: token })
|
||||
.send({ domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('add succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes')
|
||||
.send({ name: MAILBOX_NAME, userId: userId })
|
||||
@@ -803,26 +648,10 @@ describe('Mail API', function () {
|
||||
});
|
||||
|
||||
describe('aliases', function () {
|
||||
before(function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/mail')
|
||||
.query({ access_token: token })
|
||||
.send({ domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -924,30 +753,11 @@ describe('Mail API', function () {
|
||||
});
|
||||
|
||||
describe('mailinglists', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/mail')
|
||||
.query({ access_token: token })
|
||||
.send({ domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ function setup(done) {
|
||||
server.start,
|
||||
database._clear,
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain)
|
||||
], function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
|
||||
+35
-37
@@ -191,45 +191,45 @@ function initializeExpressSync() {
|
||||
|
||||
// app routes
|
||||
router.get ('/api/v1/apps', token, routes.apps.getApps);
|
||||
router.get ('/api/v1/apps/:id', token, authorizeAdmin, routes.apps.getApp);
|
||||
router.get ('/api/v1/apps/:id/icon', token, routes.apps.getAppIcon);
|
||||
router.get ('/api/v1/apps/:id', token, authorizeAdmin, routes.apps.load, routes.apps.getApp);
|
||||
router.get ('/api/v1/apps/:id/icon', token, routes.apps.load, routes.apps.getAppIcon);
|
||||
|
||||
router.post('/api/v1/apps/install', token, authorizeAdmin, routes.apps.installApp);
|
||||
router.post('/api/v1/apps/:id/uninstall', token, authorizeAdmin, routes.apps.uninstallApp);
|
||||
router.post('/api/v1/apps/install', token, authorizeAdmin, routes.apps.install);
|
||||
router.post('/api/v1/apps/:id/uninstall', token, authorizeAdmin, routes.apps.load, routes.apps.uninstall);
|
||||
|
||||
router.post('/api/v1/apps/:id/configure/access_restriction', token, authorizeAdmin, routes.apps.setAccessRestriction);
|
||||
router.post('/api/v1/apps/:id/configure/label', token, authorizeAdmin, routes.apps.setLabel);
|
||||
router.post('/api/v1/apps/:id/configure/tags', token, authorizeAdmin, routes.apps.setTags);
|
||||
router.post('/api/v1/apps/:id/configure/icon', token, authorizeAdmin, routes.apps.setIcon);
|
||||
router.post('/api/v1/apps/:id/configure/memory_limit', token, authorizeAdmin, routes.apps.setMemoryLimit);
|
||||
router.post('/api/v1/apps/:id/configure/cpu_shares', token, authorizeAdmin, routes.apps.setCpuShares);
|
||||
router.post('/api/v1/apps/:id/configure/automatic_backup', token, authorizeAdmin, routes.apps.setAutomaticBackup);
|
||||
router.post('/api/v1/apps/:id/configure/automatic_update', token, authorizeAdmin, routes.apps.setAutomaticUpdate);
|
||||
router.post('/api/v1/apps/:id/configure/reverse_proxy', token, authorizeAdmin, routes.apps.setReverseProxyConfig);
|
||||
router.post('/api/v1/apps/:id/configure/cert', token, authorizeAdmin, routes.apps.setCertificate);
|
||||
router.post('/api/v1/apps/:id/configure/debug_mode', token, authorizeAdmin, routes.apps.setDebugMode);
|
||||
router.post('/api/v1/apps/:id/configure/mailbox', token, authorizeAdmin, routes.apps.setMailbox);
|
||||
router.post('/api/v1/apps/:id/configure/env', token, authorizeAdmin, routes.apps.setEnvironment);
|
||||
router.post('/api/v1/apps/:id/configure/data_dir', token, authorizeAdmin, routes.apps.setDataDir);
|
||||
router.post('/api/v1/apps/:id/configure/location', token, authorizeAdmin, routes.apps.setLocation);
|
||||
router.post('/api/v1/apps/:id/configure/access_restriction', token, authorizeAdmin, routes.apps.load, routes.apps.setAccessRestriction);
|
||||
router.post('/api/v1/apps/:id/configure/label', token, authorizeAdmin, routes.apps.load, routes.apps.setLabel);
|
||||
router.post('/api/v1/apps/:id/configure/tags', token, authorizeAdmin, routes.apps.load, routes.apps.setTags);
|
||||
router.post('/api/v1/apps/:id/configure/icon', token, authorizeAdmin, routes.apps.load, routes.apps.setIcon);
|
||||
router.post('/api/v1/apps/:id/configure/memory_limit', token, authorizeAdmin, routes.apps.load, routes.apps.setMemoryLimit);
|
||||
router.post('/api/v1/apps/:id/configure/cpu_shares', token, authorizeAdmin, routes.apps.load, routes.apps.setCpuShares);
|
||||
router.post('/api/v1/apps/:id/configure/automatic_backup', token, authorizeAdmin, routes.apps.load, routes.apps.setAutomaticBackup);
|
||||
router.post('/api/v1/apps/:id/configure/automatic_update', token, authorizeAdmin, routes.apps.load, routes.apps.setAutomaticUpdate);
|
||||
router.post('/api/v1/apps/:id/configure/reverse_proxy', token, authorizeAdmin, routes.apps.load, routes.apps.setReverseProxyConfig);
|
||||
router.post('/api/v1/apps/:id/configure/cert', token, authorizeAdmin, routes.apps.load, routes.apps.setCertificate);
|
||||
router.post('/api/v1/apps/:id/configure/debug_mode', token, authorizeAdmin, routes.apps.load, routes.apps.setDebugMode);
|
||||
router.post('/api/v1/apps/:id/configure/mailbox', token, authorizeAdmin, routes.apps.load, routes.apps.setMailbox);
|
||||
router.post('/api/v1/apps/:id/configure/env', token, authorizeAdmin, routes.apps.load, routes.apps.setEnvironment);
|
||||
router.post('/api/v1/apps/:id/configure/data_dir', token, authorizeAdmin, routes.apps.load, routes.apps.setDataDir);
|
||||
router.post('/api/v1/apps/:id/configure/location', token, authorizeAdmin, routes.apps.load, routes.apps.setLocation);
|
||||
|
||||
router.post('/api/v1/apps/:id/repair', token, authorizeAdmin, routes.apps.repairApp);
|
||||
router.post('/api/v1/apps/:id/update', token, authorizeAdmin, routes.apps.updateApp);
|
||||
router.post('/api/v1/apps/:id/restore', token, authorizeAdmin, routes.apps.restoreApp);
|
||||
router.post('/api/v1/apps/:id/import', token, authorizeAdmin, routes.apps.importApp);
|
||||
router.post('/api/v1/apps/:id/backup', token, authorizeAdmin, routes.apps.backupApp);
|
||||
router.get ('/api/v1/apps/:id/backups', token, authorizeAdmin, routes.apps.listBackups);
|
||||
router.post('/api/v1/apps/:id/stop', token, authorizeAdmin, routes.apps.stopApp);
|
||||
router.post('/api/v1/apps/:id/start', token, authorizeAdmin, routes.apps.startApp);
|
||||
router.post('/api/v1/apps/:id/restart', token, authorizeAdmin, routes.apps.restartApp);
|
||||
router.get ('/api/v1/apps/:id/logstream', token, authorizeAdmin, routes.apps.getLogStream);
|
||||
router.get ('/api/v1/apps/:id/logs', token, authorizeAdmin, routes.apps.getLogs);
|
||||
router.get ('/api/v1/apps/:id/exec', token, authorizeAdmin, routes.apps.exec);
|
||||
router.post('/api/v1/apps/:id/repair', token, authorizeAdmin, routes.apps.load, routes.apps.repair);
|
||||
router.post('/api/v1/apps/:id/update', token, authorizeAdmin, routes.apps.load, routes.apps.update);
|
||||
router.post('/api/v1/apps/:id/restore', token, authorizeAdmin, routes.apps.load, routes.apps.restore);
|
||||
router.post('/api/v1/apps/:id/import', token, authorizeAdmin, routes.apps.load, routes.apps.importApp);
|
||||
router.post('/api/v1/apps/:id/backup', token, authorizeAdmin, routes.apps.load, routes.apps.backup);
|
||||
router.get ('/api/v1/apps/:id/backups', token, authorizeAdmin, routes.apps.load, routes.apps.listBackups);
|
||||
router.post('/api/v1/apps/:id/start', token, authorizeAdmin, routes.apps.load, routes.apps.start);
|
||||
router.post('/api/v1/apps/:id/stop', token, authorizeAdmin, routes.apps.load, routes.apps.stop);
|
||||
router.post('/api/v1/apps/:id/restart', token, authorizeAdmin, routes.apps.load, routes.apps.restart);
|
||||
router.get ('/api/v1/apps/:id/logstream', token, authorizeAdmin, routes.apps.load, routes.apps.getLogStream);
|
||||
router.get ('/api/v1/apps/:id/logs', token, authorizeAdmin, routes.apps.load, routes.apps.getLogs);
|
||||
router.get ('/api/v1/apps/:id/exec', token, authorizeAdmin, routes.apps.load, routes.apps.exec);
|
||||
// websocket cannot do bearer authentication
|
||||
router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, users.ROLE_ADMIN), routes.apps.execWebSocket);
|
||||
router.post('/api/v1/apps/:id/clone', token, authorizeAdmin, routes.apps.cloneApp);
|
||||
router.get ('/api/v1/apps/:id/download', token, authorizeAdmin, routes.apps.downloadFile);
|
||||
router.post('/api/v1/apps/:id/upload', token, authorizeAdmin, multipart, routes.apps.uploadFile);
|
||||
router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, users.ROLE_ADMIN), routes.apps.load, routes.apps.execWebSocket);
|
||||
router.post('/api/v1/apps/:id/clone', token, authorizeAdmin, routes.apps.load, routes.apps.clone);
|
||||
router.get ('/api/v1/apps/:id/download', token, authorizeAdmin, routes.apps.load, routes.apps.downloadFile);
|
||||
router.post('/api/v1/apps/:id/upload', token, authorizeAdmin, multipart, routes.apps.load, routes.apps.uploadFile);
|
||||
|
||||
router.get ('/api/v1/branding/:setting', token, authorizeOwner, routes.branding.get);
|
||||
router.post('/api/v1/branding/:setting', token, authorizeOwner, (req, res, next) => {
|
||||
@@ -253,8 +253,6 @@ function initializeExpressSync() {
|
||||
}, routes.mailserver.proxy);
|
||||
|
||||
router.get ('/api/v1/mail/:domain', token, authorizeAdmin, routes.mail.getDomain);
|
||||
router.post('/api/v1/mail', token, authorizeAdmin, routes.mail.addDomain);
|
||||
router.del ('/api/v1/mail/:domain', token, authorizeAdmin, routes.mail.removeDomain);
|
||||
router.get ('/api/v1/mail/:domain/status', token, authorizeAdmin, routes.mail.getStatus);
|
||||
router.post('/api/v1/mail/:domain/mail_from_validation', token, authorizeAdmin, routes.mail.setMailFromValidation);
|
||||
router.post('/api/v1/mail/:domain/catch_all', token, authorizeAdmin, routes.mail.setCatchAllAddress);
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ function getDisks(callback) {
|
||||
const ext4Disks = values[0].filter((r) => r.type === 'ext4').sort((a, b) => a.mountpoint.localeCompare(b.mountpoint));
|
||||
|
||||
const disks = {
|
||||
disks: ext4Disks, // root disk is first
|
||||
disks: ext4Disks, // root disk is first. { filesystem, type, size, used, avialable, capacity, mountpoint }
|
||||
boxDataDisk: values[1].filesystem,
|
||||
mailDataDisk: values[1].filesystem,
|
||||
platformDataDisk: values[2].filesystem,
|
||||
|
||||
@@ -15,6 +15,7 @@ var appdb = require('../appdb.js'),
|
||||
groupdb = require('../groupdb.js'),
|
||||
groups = require('../groups.js'),
|
||||
hat = require('../hat.js'),
|
||||
settings = require('../settings.js'),
|
||||
userdb = require('../userdb.js');
|
||||
|
||||
let AUDIT_SOURCE = { ip: '1.2.3.4' };
|
||||
@@ -173,6 +174,7 @@ describe('Apps', function () {
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear,
|
||||
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
domains.add.bind(null, DOMAIN_1.domain, DOMAIN_1, AUDIT_SOURCE),
|
||||
userdb.add.bind(null, ADMIN_0.id, ADMIN_0),
|
||||
@@ -208,6 +210,13 @@ describe('Apps', function () {
|
||||
expect(apps._validatePortBindings({ port: 1567 }, { tcpPorts: { port3: null } })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow reserved ports', function () {
|
||||
expect(apps._validatePortBindings({ port: 443 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 50000 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 51000 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 50100 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows valid bindings', function () {
|
||||
expect(apps._validatePortBindings({ port: 1024 }, { tcpPorts: { port: 5000 } })).to.be(null);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ var addons = require('../addons.js'),
|
||||
net = require('net'),
|
||||
nock = require('nock'),
|
||||
paths = require('../paths.js'),
|
||||
settings = require('../settings.js'),
|
||||
userdb = require('../userdb.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -119,6 +120,7 @@ describe('apptask', function () {
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear,
|
||||
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
userdb.add.bind(null, ADMIN.id, ADMIN),
|
||||
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.domain, APP.portBindings, APP)
|
||||
@@ -184,27 +186,6 @@ describe('apptask', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('allocate OAuth credentials', function (done) {
|
||||
addons._setupOauth(APP, {}, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('remove OAuth credentials', function (done) {
|
||||
addons._teardownOauth(APP, {}, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('remove OAuth credentials twice succeeds', function (done) {
|
||||
addons._teardownOauth(APP, {}, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('barfs on empty manifest', function (done) {
|
||||
var badApp = _.extend({ }, APP);
|
||||
badApp.manifest = { };
|
||||
|
||||
+11
-26
@@ -40,7 +40,8 @@ var USER_0 = {
|
||||
twoFactorAuthenticationSecret: '',
|
||||
role: 'user',
|
||||
active: true,
|
||||
source: ''
|
||||
source: '',
|
||||
resetTokenCreationTime: Date.now()
|
||||
};
|
||||
|
||||
var USER_1 = {
|
||||
@@ -58,7 +59,8 @@ var USER_1 = {
|
||||
twoFactorAuthenticationSecret: '',
|
||||
role: 'user',
|
||||
active: true,
|
||||
source: ''
|
||||
source: '',
|
||||
resetTokenCreationTime: Date.now()
|
||||
};
|
||||
|
||||
var USER_2 = {
|
||||
@@ -76,7 +78,8 @@ var USER_2 = {
|
||||
twoFactorAuthenticationSecret: '',
|
||||
role: 'user',
|
||||
active: true,
|
||||
source: ''
|
||||
source: '',
|
||||
resetTokenCreationTime: Date.now()
|
||||
};
|
||||
|
||||
const DOMAIN_0 = {
|
||||
@@ -980,7 +983,7 @@ describe('database', function () {
|
||||
appdb.get(APP_0.id, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an('object');
|
||||
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime'])).to.be.eql(APP_0);
|
||||
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime', 'resetTokenCreationTime'])).to.be.eql(APP_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -1020,7 +1023,7 @@ describe('database', function () {
|
||||
appdb.get(APP_0.id, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an('object');
|
||||
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime'])).to.be.eql(APP_0);
|
||||
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime','resetTokenCreationTime'])).to.be.eql(APP_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -1030,7 +1033,7 @@ describe('database', function () {
|
||||
appdb.getByHttpPort(APP_0.httpPort, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an('object');
|
||||
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime'])).to.be.eql(APP_0);
|
||||
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime','resetTokenCreationTime'])).to.be.eql(APP_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -1055,8 +1058,8 @@ describe('database', function () {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(Array);
|
||||
expect(result.length).to.be(2);
|
||||
expect(_.omit(result[0], ['creationTime', 'updateTime','ts', 'healthTime'])).to.be.eql(APP_0);
|
||||
expect(_.omit(result[1], ['creationTime', 'updateTime','ts', 'healthTime'])).to.be.eql(APP_1);
|
||||
expect(_.omit(result[0], ['creationTime', 'updateTime','ts', 'healthTime', 'resetTokenCreationTime'])).to.be.eql(APP_0);
|
||||
expect(_.omit(result[1], ['creationTime', 'updateTime','ts', 'healthTime', 'resetTokenCreationTime'])).to.be.eql(APP_1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -1774,7 +1777,6 @@ describe('database', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
domaindb.add.bind(null, DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }),
|
||||
maildb.add.bind(null, DOMAIN_0.domain, {})
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -1948,23 +1950,6 @@ describe('database', function () {
|
||||
database._clear(done);
|
||||
});
|
||||
|
||||
it('cannot add non-existing domain', function (done) {
|
||||
maildb.add(MAIL_DOMAIN_0.domain + 'nope', {}, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
expect(error.reason).to.be(BoxError.NOT_FOUND);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can add domain', function (done) {
|
||||
maildb.add(MAIL_DOMAIN_0.domain, {}, function (error) {
|
||||
expect(error).to.equal(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get all domains', function (done) {
|
||||
maildb.list(function (error, result) {
|
||||
expect(error).to.equal(null);
|
||||
|
||||
@@ -11,7 +11,7 @@ var constants = require('../constants.js'),
|
||||
exec = require('child_process').exec,
|
||||
expect = require('expect.js');
|
||||
|
||||
const DOCKER = `docker -H tcp://localhost:${constants.DOCKER_PROXY_PORT} `;
|
||||
const DOCKER = `docker -H tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT} `;
|
||||
|
||||
describe('Dockerproxy', function () {
|
||||
var containerId;
|
||||
|
||||
@@ -88,7 +88,6 @@ function setup(done) {
|
||||
database._clear.bind(null),
|
||||
ldapServer.start.bind(null),
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
maildb.add.bind(null, DOMAIN_0.domain, {}),
|
||||
function (callback) {
|
||||
users.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -34,7 +34,6 @@ function setup(done) {
|
||||
database._clear,
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain)
|
||||
], done);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,17 @@ describe('Certificates', function () {
|
||||
var validCert2 = '-----BEGIN CERTIFICATE-----\nMIIBwjCCAWwCCQCZjm6jL50XfTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wHhcN\nMTYxMTA4MDgyMDE1WhcNMjAxMTA3MDgyMDE1WjBoMQswCQYDVQQGEwJERTEPMA0G\nA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05lYnVsb24x\nDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEAtKoyTPrf2DjKbnW7Xr1HbRvV+quHTcGmUq5anDI7G4w/\nabqDXGYyakHHlPyZxYp7FWQxCm83rHUuDT1LiLIBZQIDAQABMA0GCSqGSIb3DQEB\nCwUAA0EAVaD2Q6bF9hcUUBev5NyjaMdDYURuWfjuwWUkb8W50O2ed3O+MATKrDdS\nyVaBy8W02KJ4Y1ym4je/MF8nilPurA==\n-----END CERTIFICATE-----';
|
||||
var validKey2 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBPQIBAAJBALSqMkz639g4ym51u169R20b1fqrh03BplKuWpwyOxuMP2m6g1xm\nMmpBx5T8mcWKexVkMQpvN6x1Lg09S4iyAWUCAwEAAQJBAJXu7YHPbjfuoalcUZzF\nbuKRCFtZQRf5z0Os6QvZ8A3iR0SzYJzx+c2ibp7WdifMXp3XaKm4tHSOfumrjUIq\nt10CIQDrs9Xo7bq0zuNjUV5IshNfaiYKZRfQciRVW2O8xBP9VwIhAMQ5CCEDZy+u\nsaF9RtmB0bjbe6XonBlAzoflfH/MAwWjAiEA50hL+ohr0MfCMM7DKaozgEj0kvan\n645VQLywnaX5x3kCIQDCwjinS9FnKmV0e/uOd6PJb0/S5IXLKt/TUpu33K5DMQIh\nAM9peu3B5t9pO59MmeUGZwI+bEJfEb+h03WTptBxS3pO\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
/*
|
||||
Generate these with:
|
||||
openssl ecparam -genkey -name prime256v1 -out server.key
|
||||
openssl req -new -sha256 -key server.key -out server.csr
|
||||
openssl req -x509 -sha256 -days 1460 -key server.key -in server.csr -out server.crt
|
||||
*/
|
||||
|
||||
// *.foobar.com
|
||||
var validCert4 = '-----BEGIN CERTIFICATE-----\nMIICDDCCAbOgAwIBAgIUduLaSQC6kh9LxVdua1EUBCgQOHYwCgYIKoZIzj0EAwIw\nXDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu\ndGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEVMBMGA1UEAwwMKi5mb29iYXIuY29tMB4X\nDTIwMDMyNTA0MTYxMloXDTI0MDMyNDA0MTYxMlowXDELMAkGA1UEBhMCQVUxEzAR\nBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5\nIEx0ZDEVMBMGA1UEAwwMKi5mb29iYXIuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0D\nAQcDQgAEmBum8MbyGXKuLP+NEOmR15XlemPEHR4b68A+B0Zjh/cuLQncAIwfmLT7\nutUOh3CivEKvZYkQIdd71xhCbVtbkqNTMFEwHQYDVR0OBBYEFCxEvAFsSFyAITNw\niBttbdsyEwO4MB8GA1UdIwQYMBaAFCxEvAFsSFyAITNwiBttbdsyEwO4MA8GA1Ud\nEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgd+rxp8xTXy7wsV45hiu1HQ2p\nwrEEPFmfPinVHwhDCiECIAEnIr5bEYUzSjujiHg7C2q3zh41XJhZWQie3VHLY/Kt\n-----END CERTIFICATE-----\n';
|
||||
var validKey4 = '-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAXuQG4YDaQuwOCvWOZjkOvw/Y5V8Oum+rWnliMTsA5woAoGCCqGSM49\nAwEHoUQDQgAEmBum8MbyGXKuLP+NEOmR15XlemPEHR4b68A+B0Zjh/cuLQncAIwf\nmLT7utUOh3CivEKvZYkQIdd71xhCbVtbkg==\n-----END EC PRIVATE KEY-----\n';
|
||||
|
||||
// cp /etc/ssl/openssl.cnf /tmp/openssl.cnf
|
||||
// echo -e "[SAN]\nsubjectAltName=DNS:amazing.com,DNS:*.amazing.com\n" >> /tmp/openssl.cnf
|
||||
// openssl req -x509 -newkey rsa:2048 -keyout amazing.key -out amazing.crt -days 3650 -subj /CN=*.amazing.com -nodes -extensions SAN -config /tmp/openssl.cnf
|
||||
@@ -123,6 +134,10 @@ describe('Certificates', function () {
|
||||
expect(reverseProxy.validateCertificate('', amazingDomain, { cert: validCert3, key: validKey3 })).to.be(null);
|
||||
expect(reverseProxy.validateCertificate('subdomain', amazingDomain, { cert: validCert3, key: validKey3 })).to.be(null);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain (subdomain) - ecdsa', function () {
|
||||
expect(reverseProxy.validateCertificate('baz', foobarDomain, { cert: validCert4, key: validKey4 })).to.be(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateFallbackCertificiate - non-hyphenated', function () {
|
||||
|
||||
@@ -82,7 +82,6 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
cron.startJobs,
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain),
|
||||
users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
|
||||
settings.setBoxAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
|
||||
settingsdb.set.bind(null, settings.CLOUDRON_TOKEN_KEY, 'atoken'),
|
||||
@@ -154,7 +153,6 @@ describe('updatechecker - box - automatic (no email)', function () {
|
||||
cron.startJobs,
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain),
|
||||
users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
|
||||
settingsdb.set.bind(null, settings.CLOUDRON_TOKEN_KEY, 'atoken'),
|
||||
], done);
|
||||
@@ -190,7 +188,6 @@ describe('updatechecker - box - automatic free (email)', function () {
|
||||
cron.startJobs,
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain),
|
||||
users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
|
||||
settingsdb.set.bind(null, settings.CLOUDRON_TOKEN_KEY, 'atoken'),
|
||||
], done);
|
||||
@@ -254,7 +251,6 @@ describe('updatechecker - app - manual (email)', function () {
|
||||
cron.startJobs,
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain),
|
||||
users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0),
|
||||
settings.setAppAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
|
||||
@@ -364,7 +360,6 @@ describe('updatechecker - app - automatic (no email)', function () {
|
||||
cron.startJobs,
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain),
|
||||
users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0),
|
||||
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
|
||||
@@ -430,7 +425,6 @@ describe('updatechecker - app - automatic free (email)', function () {
|
||||
cron.startJobs,
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain),
|
||||
users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0),
|
||||
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
|
||||
|
||||
@@ -74,7 +74,6 @@ function setup(done) {
|
||||
database._clear,
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain),
|
||||
], done);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ var assert = require('assert'),
|
||||
mysql = require('mysql');
|
||||
|
||||
var USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'createdAt', 'modifiedAt', 'resetToken', 'displayName',
|
||||
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role' ].join(',');
|
||||
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime' ].join(',');
|
||||
|
||||
var APP_PASSWORD_FIELDS = [ 'id', 'name', 'userId', 'identifier', 'hashedPassword', 'creationTime' ].join(',');
|
||||
|
||||
|
||||
+3
-2
@@ -495,10 +495,11 @@ function resetPasswordByIdentifier(identifier, callback) {
|
||||
getter(identifier.toLowerCase(), function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let resetToken = hat(256);
|
||||
let resetToken = hat(256), resetTokenCreationTime = new Date();
|
||||
result.resetToken = resetToken;
|
||||
result.resetTokenCreationTime = resetTokenCreationTime;
|
||||
|
||||
userdb.update(result.id, { resetToken }, function (error) {
|
||||
userdb.update(result.id, { resetToken, resetTokenCreationTime }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mailer.passwordReset(result);
|
||||
|
||||
Reference in New Issue
Block a user