Compare commits
98 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 | |||
| d3eeb5f48a | |||
| 1e8a02f91a | |||
| 97c3bd8b8e | |||
| 09ce27d74b | |||
| 2447e91a9f | |||
| e6d881b75d | |||
| 36f963dce8 | |||
| 1b15d28212 | |||
| 4e0c15e102 | |||
| c9e40f59de | |||
| 38cf31885c | |||
| 4420470242 | |||
| 9b05786615 | |||
| 725b2c81ee | |||
| 661965f2e0 | |||
| 7e0ef60305 | |||
| 2ac0fe21c6 | |||
| b997f2329d | |||
| 23ee758ac9 | |||
| 9ea12e71f0 | |||
| d3594c2dd6 | |||
| 6ee4b0da27 | |||
| 3e66feb514 | |||
| cd91a5ef64 | |||
| cf89609633 |
@@ -1821,3 +1821,81 @@
|
||||
* restore: carefully replace backup config
|
||||
* spam: per mailbox bayes db and training
|
||||
|
||||
[5.0.3]
|
||||
* Show backup disk usage in graphs
|
||||
* Add per-user app passwords
|
||||
* Make app not responding page customizable
|
||||
* Make footer customizable
|
||||
* Add UI to import backups
|
||||
* Display timestamps in browser timezone in the UI
|
||||
* Mail eventlog and usage
|
||||
* Add user roles - owner, admin, user manager and user
|
||||
* Setup logrotate configs for collectd since upstream does not set it up
|
||||
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
|
||||
* linode: add object storage backend
|
||||
* restore: carefully replace backup config
|
||||
* spam: per mailbox bayes db and training
|
||||
|
||||
[5.0.4]
|
||||
* Fix potential previlige escalation because of ghost file
|
||||
* linode: dns backend
|
||||
* make branding routes owner only
|
||||
* add branding API
|
||||
* Add app start/stop/restart events
|
||||
* Use the primary email for LE account
|
||||
* make mail eventlog more descriptive
|
||||
|
||||
[5.0.5]
|
||||
* Fix bug where incoming mail from dynamic hostnames was rejected
|
||||
* Increase token expiry
|
||||
* Fix bug in tag UI where tag removal did not work
|
||||
|
||||
[5.0.6]
|
||||
* 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,10 +34,15 @@ 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)
|
||||
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > /tmp/cloudron_ghost.json
|
||||
echo "Login as ${admin_username} / ${admin_password} . Remove /tmp/cloudron_ghost.json when done."
|
||||
ghost_file=/home/yellowtent/platformdata/cloudron_ghost.json
|
||||
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > "${ghost_file}"
|
||||
chown yellowtent:yellowtent "${ghost_file}" && chmod o-r,g-r "${ghost_file}"
|
||||
echo "Login as ${admin_username} / ${admin_password} . Remove ${ghost_file} when done."
|
||||
exit 0
|
||||
;;
|
||||
--) break;;
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# add customizations here
|
||||
# after making changes run "sudo systemctl restart box"
|
||||
|
||||
# appstore:
|
||||
# blacklist:
|
||||
# - io.wekan.cloudronapp
|
||||
# - io.cloudron.openvpn
|
||||
# whitelist:
|
||||
# org.wordpress.cloudronapp: {}
|
||||
# chat.rocket.cloudronapp: {}
|
||||
# com.nextcloud.cloudronapp: {}
|
||||
#
|
||||
# backups:
|
||||
# configurable: true
|
||||
#
|
||||
# domains:
|
||||
# dynamicDns: true
|
||||
# changeDashboardDomain: true
|
||||
#
|
||||
# subscription:
|
||||
# configurable: true
|
||||
#
|
||||
# support:
|
||||
# email: support@cloudron.io
|
||||
# remoteSupport: true
|
||||
#
|
||||
# ticketFormBody: |
|
||||
# Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).
|
||||
# * [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)
|
||||
# * [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)
|
||||
# * [Forum](https://forum.cloudron.io/)
|
||||
#
|
||||
# submitTickets: true
|
||||
#
|
||||
# alerts:
|
||||
# email: support@cloudron.io
|
||||
# notifyCloudronAdmins: false
|
||||
#
|
||||
# footer:
|
||||
# body: '© 2020 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
|
||||
+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');
|
||||
|
||||
|
||||
+774
-828
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -431,12 +431,12 @@ function waitForDnsPropagation(app, callback) {
|
||||
sysinfo.getServerIp(function (error, ip) {
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Error getting public IP: ${error.message}`));
|
||||
|
||||
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
|
||||
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { times: 240 }, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: app.location, domain: app.domain }));
|
||||
|
||||
// now wait for alternateDomains, if any
|
||||
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
|
||||
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
|
||||
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { times: 240 }, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: domain.subdomain, domain: domain.domain }));
|
||||
|
||||
iteratorCallback();
|
||||
|
||||
+1
-1
@@ -452,7 +452,7 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
|
||||
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
|
||||
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, challenge);
|
||||
|
||||
+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);
|
||||
});
|
||||
|
||||
+1
-3
@@ -32,9 +32,7 @@ exports = module.exports = {
|
||||
|
||||
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
|
||||
|
||||
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
|
||||
|
||||
DEFAULT_TOKEN_EXPIRATION: 7 * 24 * 60 * 60 * 1000, // 1 week
|
||||
DEFAULT_TOKEN_EXPIRATION: 365 * 24 * 60 * 60 * 1000, // 1 year
|
||||
|
||||
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js
|
||||
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
removePrivateFields: removePrivateFields,
|
||||
injectPrivateFields: injectPrivateFields,
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
wait: wait,
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
let async = require('async'),
|
||||
assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
debug = require('debug')('box:dns/linode'),
|
||||
dns = require('../native-dns.js'),
|
||||
domains = require('../domains.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
const LINODE_ENDPOINT = 'https://api.linode.com/v4';
|
||||
|
||||
function formatError(response) {
|
||||
return util.format('Linode DNS error [%s] %j', response.statusCode, response.body);
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function getZoneId(dnsConfig, zoneName, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// returns 100 at a time
|
||||
superagent.get(`${LINODE_ENDPOINT}/domains`)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
if (!Array.isArray(result.body.data)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
|
||||
const zone = result.body.data.find(d => d.domain === zoneName);
|
||||
|
||||
if (!zone || !zone.id) return callback(new BoxError(BoxError.NOT_FOUND, 'Zone not found'));
|
||||
|
||||
debug(`getZoneId: zone id of ${zoneName} is ${zone.id}`);
|
||||
|
||||
callback(null, zone.id);
|
||||
});
|
||||
}
|
||||
|
||||
function getZoneRecords(dnsConfig, zoneName, name, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`getInternal: getting dns records of ${zoneName} with ${name} and type ${type}`);
|
||||
|
||||
getZoneId(dnsConfig, zoneName, function (error, zoneId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let page = 0, more = false;
|
||||
let records = [];
|
||||
|
||||
async.doWhilst(function (iteratorDone) {
|
||||
const url = `${LINODE_ENDPOINT}/domains/${zoneId}/records?page=${++page}`;
|
||||
|
||||
superagent.get(url)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return iteratorDone(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 404) return iteratorDone(new BoxError(BoxError.NOT_FOUND, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return iteratorDone(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return iteratorDone(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
records = records.concat(result.body.data.filter(function (record) {
|
||||
return (record.type === type && record.name === name);
|
||||
}));
|
||||
|
||||
more = result.body.page !== result.body.pages;
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () { return more; }, function (error) {
|
||||
debug('getZoneRecords:', error, JSON.stringify(records));
|
||||
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, { zoneId, records });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function get(domainObject, location, type, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { records }) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var tmp = records.map(function (record) { return record.target; });
|
||||
|
||||
debug('get: %j', tmp);
|
||||
|
||||
return callback(null, tmp);
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(domainObject, location, type, values, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
|
||||
|
||||
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let i = 0, recordIds = []; // used to track available records to update instead of create
|
||||
|
||||
async.eachSeries(values, function (value, iteratorCallback) {
|
||||
let data = {
|
||||
type: type,
|
||||
ttl_sec: 300 // lowest
|
||||
};
|
||||
|
||||
if (type === 'MX') {
|
||||
data.priority = parseInt(value.split(' ')[0], 10);
|
||||
data.target = value.split(' ')[1];
|
||||
} else if (type === 'TXT') {
|
||||
data.target = value.replace(/^"(.*)"$/, '$1'); // strip any double quotes
|
||||
} else {
|
||||
data.target = value;
|
||||
}
|
||||
|
||||
if (i >= records.length) {
|
||||
data.name = name; // only set for new records
|
||||
|
||||
superagent.post(`${LINODE_ENDPOINT}/domains/${zoneId}/records`)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
recordIds.push(result.body.id);
|
||||
|
||||
return iteratorCallback(null);
|
||||
});
|
||||
} else {
|
||||
superagent.put(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${records[i].id}`)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
// increment, as we have consumed the record
|
||||
++i;
|
||||
|
||||
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
recordIds.push(result.body.id);
|
||||
|
||||
return iteratorCallback(null);
|
||||
});
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('upsert: completed with recordIds:%j', recordIds);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function del(domainObject, location, type, values, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (records.length === 0) return callback(null);
|
||||
|
||||
var tmp = records.filter(function (record) { return values.some(function (value) { return value === record.target; }); });
|
||||
|
||||
debug('del: %j', tmp);
|
||||
|
||||
if (tmp.length === 0) return callback(null);
|
||||
|
||||
// FIXME we only handle the first one currently
|
||||
|
||||
superagent.del(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${tmp[0].id}`)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(null);
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('del: done');
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function wait(domainObject, location, type, value, options, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
|
||||
}
|
||||
|
||||
function verifyDnsConfig(domainObject, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName;
|
||||
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
|
||||
|
||||
const ip = '127.0.0.1';
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
|
||||
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
|
||||
|
||||
if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.linode.com') === -1) {
|
||||
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
|
||||
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Linode', { field: 'nameservers' }));
|
||||
}
|
||||
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added');
|
||||
|
||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+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);
|
||||
});
|
||||
|
||||
+15
-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');
|
||||
@@ -60,6 +63,7 @@ function api(provider) {
|
||||
case 'digitalocean': return require('./dns/digitalocean.js');
|
||||
case 'gandi': return require('./dns/gandi.js');
|
||||
case 'godaddy': return require('./dns/godaddy.js');
|
||||
case 'linode': return require('./dns/linode.js');
|
||||
case 'namecom': return require('./dns/namecom.js');
|
||||
case 'namecheap': return require('./dns/namecheap.js');
|
||||
case 'noop': return require('./dns/noop.js');
|
||||
@@ -170,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' }));
|
||||
@@ -193,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) {
|
||||
@@ -204,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();
|
||||
});
|
||||
});
|
||||
@@ -313,6 +321,8 @@ function del(domain, auditSource, callback) {
|
||||
|
||||
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
|
||||
|
||||
mail.onDomainRemoved(domain, NOOP_CALLBACK);
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
@@ -436,6 +446,9 @@ function waitForDnsRecord(location, domain, type, value, options, callback) {
|
||||
get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// linode DNS takes ~15mins
|
||||
if (!options.interval) options.interval = domainObject.provider === 'linode' ? 20000 : 5000;
|
||||
|
||||
api(domainObject.provider).wait(domainObject, location, type, value, options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
+4
-1
@@ -7,7 +7,7 @@ exports = module.exports = {
|
||||
getByCreationTime: getByCreationTime,
|
||||
cleanup: cleanup,
|
||||
|
||||
// keep in sync with webadmin index.js filter and CLI tool
|
||||
// keep in sync with webadmin index.js filter
|
||||
ACTION_ACTIVATE: 'cloudron.activate',
|
||||
ACTION_APP_CLONE: 'app.clone',
|
||||
ACTION_APP_CONFIGURE: 'app.configure',
|
||||
@@ -21,6 +21,9 @@ exports = module.exports = {
|
||||
ACTION_APP_OOM: 'app.oom',
|
||||
ACTION_APP_UP: 'app.up',
|
||||
ACTION_APP_DOWN: 'app.down',
|
||||
ACTION_APP_START: 'app.start',
|
||||
ACTION_APP_STOP: 'app.stop',
|
||||
ACTION_APP_RESTART: 'app.restart',
|
||||
|
||||
ACTION_BACKUP_FINISH: 'backup.finish',
|
||||
ACTION_BACKUP_START: 'backup.start',
|
||||
|
||||
@@ -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.2@sha256:e7847f626bac91698dd7d779048d9b1ee4e1764d1399f2d0553da4882c272166' },
|
||||
'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' }
|
||||
}
|
||||
};
|
||||
|
||||
+7
-4
@@ -534,13 +534,16 @@ function authenticateSftp(req, res, next) {
|
||||
var parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
// actual user bind
|
||||
users.verifyWithUsername(parts[0], req.credentials, users.AP_SFTP, function (error) {
|
||||
apps.getByFqdn(parts[1], function (error, app) {
|
||||
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
|
||||
debug('sftp auth: success');
|
||||
users.verifyWithUsername(parts[0], req.credentials, app.id, function (error) {
|
||||
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
|
||||
res.end();
|
||||
debug('sftp auth: success');
|
||||
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+12
-29
@@ -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,
|
||||
@@ -101,7 +102,6 @@ function checkOutboundPort25(callback) {
|
||||
'smtp.gmail.com',
|
||||
'smtp.live.com',
|
||||
'smtp.mail.yahoo.com',
|
||||
'smtp.comcast.net',
|
||||
'smtp.1und1.de',
|
||||
]);
|
||||
|
||||
@@ -313,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);
|
||||
@@ -906,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";
|
||||
|
||||
|
||||
+4
-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,11 +46,14 @@ 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'),
|
||||
CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'),
|
||||
|
||||
GHOST_USER_FILE: path.join(baseDir(), 'platformdata/cloudron_ghost.json'),
|
||||
|
||||
// this pattern is for the cloudron logs API route to work
|
||||
BACKUP_LOG_FILE: path.join(baseDir(), 'platformdata/logs/backup/app.log'),
|
||||
UPDATER_LOG_FILE: path.join(baseDir(), 'platformdata/logs/updater/app.log')
|
||||
|
||||
+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) {
|
||||
|
||||
+8
-8
@@ -79,7 +79,7 @@ function getCertApi(domainObject, callback) {
|
||||
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
users.getOwner(function (error, owner) {
|
||||
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
|
||||
options.email = error ? 'support@cloudron.io' : owner.email; // can error if not activated yet
|
||||
|
||||
callback(null, api, options);
|
||||
});
|
||||
@@ -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
-133
@@ -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,22 +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'));
|
||||
|
||||
debug('Installing app :%j', data);
|
||||
|
||||
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, {}));
|
||||
@@ -157,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, {}));
|
||||
@@ -170,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, {}));
|
||||
@@ -184,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, {}));
|
||||
@@ -197,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 }));
|
||||
@@ -210,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 }));
|
||||
@@ -223,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, {}));
|
||||
@@ -236,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, {}));
|
||||
@@ -249,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, {}));
|
||||
@@ -264,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, {}));
|
||||
@@ -280,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 }));
|
||||
@@ -294,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 }));
|
||||
@@ -307,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 }));
|
||||
@@ -321,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'));
|
||||
@@ -336,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 }));
|
||||
@@ -345,51 +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');
|
||||
|
||||
debug('Repair app id:%s', req.params.id);
|
||||
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;
|
||||
|
||||
debug('Restore app id:%s', req.params.id);
|
||||
|
||||
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 }));
|
||||
@@ -398,12 +413,10 @@ 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;
|
||||
|
||||
debug('Importing app id:%s', req.params.id);
|
||||
|
||||
if ('backupId' in data) { // if not provided, we import in-place
|
||||
if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string'));
|
||||
if (typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string'));
|
||||
@@ -422,21 +435,19 @@ 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;
|
||||
|
||||
debug('Clone app id:%s', req.params.id);
|
||||
|
||||
if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string'));
|
||||
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
|
||||
if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required'));
|
||||
@@ -444,76 +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');
|
||||
|
||||
debug('Backup app id:%s', req.params.id);
|
||||
|
||||
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');
|
||||
|
||||
debug('Uninstalling app id:%s', req.params.id);
|
||||
|
||||
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');
|
||||
|
||||
debug('Start app id:%s', req.params.id);
|
||||
|
||||
apps.start(req.params.id, 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');
|
||||
|
||||
debug('Stop app id:%s', req.params.id);
|
||||
|
||||
apps.stop(req.params.id, 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');
|
||||
|
||||
debug('Restart app id:%s', req.params.id);
|
||||
|
||||
apps.restart(req.params.id, 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;
|
||||
|
||||
@@ -525,20 +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'));
|
||||
|
||||
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
|
||||
|
||||
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');
|
||||
|
||||
debug('Getting logstream of app id:%s', req.params.id);
|
||||
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'));
|
||||
@@ -553,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, {
|
||||
@@ -575,20 +580,18 @@ 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'));
|
||||
|
||||
debug('Getting logs of app id:%s', req.params.id);
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: false,
|
||||
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, {
|
||||
@@ -624,9 +627,7 @@ function demuxStream(stream, stdin) {
|
||||
}
|
||||
|
||||
function exec(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Execing into app id:%s and cmd:%s', req.params.id, req.query.cmd);
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
var cmd = null;
|
||||
if (req.query.cmd) {
|
||||
@@ -640,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'));
|
||||
@@ -664,9 +667,7 @@ function exec(req, res, next) {
|
||||
}
|
||||
|
||||
function execWebSocket(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Execing websocket into app id:%s and cmd:%s', req.params.id, req.query.cmd);
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
var cmd = null;
|
||||
if (req.query.cmd) {
|
||||
@@ -682,11 +683,9 @@ 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));
|
||||
|
||||
debug('Connected to terminal');
|
||||
|
||||
req.clearTimeout();
|
||||
|
||||
res.handleUpgrade(function (ws) {
|
||||
@@ -714,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'));
|
||||
@@ -722,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 }));
|
||||
@@ -730,30 +729,24 @@ function listBackups(req, res, next) {
|
||||
}
|
||||
|
||||
function uploadFile(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('uploadFile: %s %j -> %s', req.params.id, req.files, req.query.file);
|
||||
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));
|
||||
|
||||
debug('uploadFile: done');
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function downloadFile(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('downloadFile: ', req.params.id, req.query.file);
|
||||
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 = {
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get,
|
||||
set,
|
||||
|
||||
getCloudronAvatar
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
safe = require('safetydance'),
|
||||
settings = require('../settings.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
function getFooter(req, res, next) {
|
||||
settings.getFooter(function (error, footer) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, { footer }));
|
||||
});
|
||||
}
|
||||
|
||||
function setFooter(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.footer !== 'string') return next(new HttpError(400, 'footer is required'));
|
||||
|
||||
settings.setFooter(req.body.footer, function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function setCloudronName(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name is required'));
|
||||
|
||||
settings.setCloudronName(req.body.name, function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getCloudronName(req, res, next) {
|
||||
settings.getCloudronName(function (error, name) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, { name: name }));
|
||||
});
|
||||
}
|
||||
|
||||
function setAppstoreListingConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
const listingConfig = _.pick(req.body, 'whitelist', 'blacklist');
|
||||
if (Object.keys(listingConfig).length === 0) return next(new HttpError(400, 'blacklist or whitelist is required'));
|
||||
|
||||
if ('whitelist' in listingConfig) {
|
||||
if (listingConfig.whitelist !== null && !Array.isArray(listingConfig.whitelist)) return next(new HttpError(400, 'whitelist is null or an array of strings'));
|
||||
|
||||
if (listingConfig.whitelist && !listingConfig.whitelist.every(id => typeof id === 'string')) return next(new HttpError(400, 'whitelist must be array of strings'));
|
||||
}
|
||||
|
||||
if ('blacklist' in listingConfig) {
|
||||
if (!Array.isArray(listingConfig.blacklist)) return next(new HttpError(400, 'blacklist an array of strings'));
|
||||
|
||||
if (!listingConfig.blacklist.every(id => typeof id === 'string')) return next(new HttpError(400, 'blacklist must be array of strings'));
|
||||
}
|
||||
|
||||
settings.setAppstoreListingConfig(listingConfig, function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getAppstoreListingConfig(req, res, next) {
|
||||
settings.getAppstoreListingConfig(function (error, listingConfig) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, listingConfig));
|
||||
});
|
||||
}
|
||||
|
||||
function setCloudronAvatar(req, res, next) {
|
||||
assert.strictEqual(typeof req.files, 'object');
|
||||
|
||||
if (!req.files.avatar) return next(new HttpError(400, 'avatar must be provided'));
|
||||
var avatar = safe.fs.readFileSync(req.files.avatar.path);
|
||||
|
||||
settings.setCloudronAvatar(avatar, function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getCloudronAvatar(req, res, next) {
|
||||
settings.getCloudronAvatar(function (error, avatar) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
// avoid caching the avatar on the client to see avatar changes immediately
|
||||
res.set('Cache-Control', 'no-cache');
|
||||
|
||||
res.set('Content-Type', 'image/png');
|
||||
res.status(200).send(avatar);
|
||||
});
|
||||
}
|
||||
|
||||
function get(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.setting, 'string');
|
||||
|
||||
switch (req.params.setting) {
|
||||
case settings.APPSTORE_LISTING_CONFIG_KEY: return getAppstoreListingConfig(req, res, next);
|
||||
case settings.CLOUDRON_AVATAR_KEY: return getCloudronAvatar(req, res, next);
|
||||
case settings.CLOUDRON_NAME_KEY: return getCloudronName(req, res, next);
|
||||
case settings.FOOTER_KEY: return getFooter(req, res, next);
|
||||
|
||||
default: return next(new HttpError(404, 'No such setting'));
|
||||
}
|
||||
}
|
||||
|
||||
function set(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
switch (req.params.setting) {
|
||||
case settings.APPSTORE_LISTING_CONFIG_KEY: return setAppstoreListingConfig(req, res, next);
|
||||
case settings.CLOUDRON_AVATAR_KEY: return setCloudronAvatar(req, res, next);
|
||||
case settings.CLOUDRON_NAME_KEY: return setCloudronName(req, res, next);
|
||||
case settings.FOOTER_KEY: return setFooter(req, res, next);
|
||||
|
||||
default: return next(new HttpError(404, 'No such branding'));
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ let assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
cloudron = require('../cloudron.js'),
|
||||
constants = require('../constants.js'),
|
||||
debug = require('debug')('box:routes/cloudron'),
|
||||
eventlog = require('../eventlog.js'),
|
||||
externalLdap = require('../externalldap.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
@@ -103,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
|
||||
@@ -128,11 +128,11 @@ function setupAccount(req, res, next) {
|
||||
if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string'));
|
||||
if (!req.body.displayName || typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string'));
|
||||
|
||||
debug(`setupAccount: for email ${req.body.email} and username ${req.body.username} with token ${req.body.resetToken}`);
|
||||
|
||||
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));
|
||||
|
||||
@@ -6,6 +6,7 @@ exports = module.exports = {
|
||||
apps: require('./apps.js'),
|
||||
appstore: require('./appstore.js'),
|
||||
backups: require('./backups.js'),
|
||||
branding: require('./branding.js'),
|
||||
cloudron: require('./cloudron.js'),
|
||||
domains: require('./domains.js'),
|
||||
eventlog: require('./eventlog.js'),
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ exports = module.exports = {
|
||||
var addons = require('../addons.js'),
|
||||
assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
debug = require('debug')('box:routes/addons'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
@@ -60,8 +59,6 @@ function getLogs(req, res, next) {
|
||||
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 number'));
|
||||
|
||||
debug(`Getting logs of service ${req.params.service}`);
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: false,
|
||||
@@ -85,8 +82,6 @@ function getLogs(req, res, next) {
|
||||
function getLogStream(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
debug(`Getting logstream of service ${req.params.service}`);
|
||||
|
||||
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'));
|
||||
|
||||
@@ -124,8 +119,6 @@ function getLogStream(req, res, next) {
|
||||
function restart(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
debug(`Restarting service ${req.params.service}`);
|
||||
|
||||
addons.restartService(req.params.service, function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
|
||||
+4
-80
@@ -1,10 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
set: set,
|
||||
get: get,
|
||||
set,
|
||||
get,
|
||||
|
||||
getCloudronAvatar: getCloudronAvatar
|
||||
// owner only settings
|
||||
setBackupConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -14,7 +15,6 @@ var assert = require('assert'),
|
||||
externalLdap = require('../externalldap.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
safe = require('safetydance'),
|
||||
settings = require('../settings.js');
|
||||
|
||||
function getAppAutoupdatePattern(req, res, next) {
|
||||
@@ -57,26 +57,6 @@ function setBoxAutoupdatePattern(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function setCloudronName(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name is required'));
|
||||
|
||||
settings.setCloudronName(req.body.name, function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getCloudronName(req, res, next) {
|
||||
settings.getCloudronName(function (error, name) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, { name: name }));
|
||||
});
|
||||
}
|
||||
|
||||
function getTimeZone(req, res, next) {
|
||||
settings.getTimeZone(function (error, tz) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
@@ -97,26 +77,6 @@ function setTimeZone(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function getFooter(req, res, next) {
|
||||
settings.getFooter(function (error, footer) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, { footer }));
|
||||
});
|
||||
}
|
||||
|
||||
function setFooter(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.footer !== 'string') return next(new HttpError(400, 'footer is required'));
|
||||
|
||||
settings.setFooter(req.body.footer, function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getSupportConfig(req, res, next) {
|
||||
settings.getSupportConfig(function (error, supportConfig) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
@@ -125,31 +85,6 @@ function getSupportConfig(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function setCloudronAvatar(req, res, next) {
|
||||
assert.strictEqual(typeof req.files, 'object');
|
||||
|
||||
if (!req.files.avatar) return next(new HttpError(400, 'avatar must be provided'));
|
||||
var avatar = safe.fs.readFileSync(req.files.avatar.path);
|
||||
|
||||
settings.setCloudronAvatar(avatar, function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getCloudronAvatar(req, res, next) {
|
||||
settings.getCloudronAvatar(function (error, avatar) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
// avoid caching the avatar on the client to see avatar changes immediately
|
||||
res.set('Cache-Control', 'no-cache');
|
||||
|
||||
res.set('Content-Type', 'image/png');
|
||||
res.status(200).send(avatar);
|
||||
});
|
||||
}
|
||||
|
||||
function getBackupConfig(req, res, next) {
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
@@ -332,11 +267,6 @@ function get(req, res, next) {
|
||||
case settings.APP_AUTOUPDATE_PATTERN_KEY: return getAppAutoupdatePattern(req, res, next);
|
||||
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return getBoxAutoupdatePattern(req, res, next);
|
||||
case settings.TIME_ZONE_KEY: return getTimeZone(req, res, next);
|
||||
case settings.CLOUDRON_NAME_KEY: return getCloudronName(req, res, next);
|
||||
|
||||
case settings.FOOTER_KEY: return getFooter(req, res, next);
|
||||
|
||||
case settings.CLOUDRON_AVATAR_KEY: return getCloudronAvatar(req, res, next);
|
||||
|
||||
case settings.SUPPORT_CONFIG_KEY: return getSupportConfig(req, res, next);
|
||||
|
||||
@@ -349,7 +279,6 @@ function set(req, res, next) {
|
||||
|
||||
switch (req.params.setting) {
|
||||
case settings.DYNAMIC_DNS_KEY: return setDynamicDnsConfig(req, res, next);
|
||||
case settings.BACKUP_CONFIG_KEY: return setBackupConfig(req, res, next);
|
||||
case settings.PLATFORM_CONFIG_KEY: return setPlatformConfig(req, res, next);
|
||||
case settings.EXTERNAL_LDAP_KEY: return setExternalLdapConfig(req, res, next);
|
||||
case settings.UNSTABLE_APPS_KEY: return setUnstableAppsConfig(req, res, next);
|
||||
@@ -359,11 +288,6 @@ function set(req, res, next) {
|
||||
case settings.APP_AUTOUPDATE_PATTERN_KEY: return setAppAutoupdatePattern(req, res, next);
|
||||
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return setBoxAutoupdatePattern(req, res, next);
|
||||
case settings.TIME_ZONE_KEY: return setTimeZone(req, res, next);
|
||||
case settings.CLOUDRON_NAME_KEY: return setCloudronName(req, res, next);
|
||||
|
||||
case settings.FOOTER_KEY: return setFooter(req, res, next);
|
||||
|
||||
case settings.CLOUDRON_AVATAR_KEY: return setCloudronAvatar(req, res, next);
|
||||
|
||||
default: return next(new HttpError(404, 'No such setting'));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
'use strict';
|
||||
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
var async = require('async'),
|
||||
constants = require('../../constants.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
paths = require('../../paths.js'),
|
||||
server = require('../../server.js'),
|
||||
superagent = require('superagent');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + constants.PORT;
|
||||
|
||||
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null;
|
||||
|
||||
function setup(done) {
|
||||
async.series([
|
||||
server.start.bind(null),
|
||||
database._clear.bind(null),
|
||||
|
||||
function createAdmin(callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Branding API', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
describe('cloudron_name', function () {
|
||||
var name = 'foobar';
|
||||
|
||||
it('get default succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/branding/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.name).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set without name', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set empty name', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.send({ name: '' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.send({ name: name })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/branding/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.name).to.eql(name);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloudron_avatar', function () {
|
||||
it('get default succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/branding/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body).to.be.a(Buffer);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set without data', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.attach('avatar', paths.CLOUDRON_DEFAULT_AVATAR_FILE)
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/branding/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.toString()).to.eql(fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE, 'utf-8'));
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('appstore listing config', function () {
|
||||
it('get default succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/branding/appstore_listing_config')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.whitelist).to.eql(null);
|
||||
expect(res.body.blacklist).to.eql([]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set with no bl or wl', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set bad bl', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
|
||||
.query({ access_token: token })
|
||||
.send({ blacklist: [ 1 ] })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set bad wl', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
|
||||
.query({ access_token: token })
|
||||
.send({ whitelist: 4 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set bl succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
|
||||
.query({ access_token: token })
|
||||
.send({ blacklist: [ 'id1', 'id2' ] })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get bl succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/branding/appstore_listing_config')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.blacklist).to.eql([ 'id1', 'id2' ]);
|
||||
expect(res.body.whitelist).to.be(undefined);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set wl succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
|
||||
.query({ access_token: token })
|
||||
.send({ whitelist: [ 'id1', 'id2' ] })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get wl succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/branding/appstore_listing_config')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.whitelist).to.eql([ 'id1', 'id2' ]);
|
||||
expect(res.body.blacklist).to.be(undefined);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ var async = require('async'),
|
||||
constants = require('../../constants.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
paths = require('../../paths.js'),
|
||||
server = require('../../server.js'),
|
||||
superagent = require('superagent');
|
||||
|
||||
@@ -195,100 +193,6 @@ describe('Settings API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloudron_name', function () {
|
||||
var name = 'foobar';
|
||||
|
||||
it('get default succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.name).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set without name', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set empty name', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.send({ name: '' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.send({ name: name })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.name).to.eql(name);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloudron_avatar', function () {
|
||||
it('get default succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body).to.be.a(Buffer);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set without data', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.attach('avatar', paths.CLOUDRON_DEFAULT_AVATAR_FILE)
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.toString()).to.eql(fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE, 'utf-8'));
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('time_zone', function () {
|
||||
it('succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/time_zone')
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -116,7 +115,7 @@ describe('Users API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('create admin', function (done) {
|
||||
it('create owner', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME_0, password: PASSWORD, email: EMAIL_0 })
|
||||
|
||||
+3
-1
@@ -71,6 +71,8 @@ function update(req, res, next) {
|
||||
if ('role' in req.body) {
|
||||
if (typeof req.body.role !== 'string') return next(new HttpError(400, 'role must be a string'));
|
||||
if (req.user.id === req.resource.id) return next(new HttpError(409, 'Cannot set role flag on self'));
|
||||
|
||||
if (users.compareRoles(req.user.role, req.body.role) < 0) return next(new HttpError(403, `role '${req.body.role}' is required but you are only '${req.user.role}'`));
|
||||
}
|
||||
|
||||
if ('active' in req.body) {
|
||||
@@ -78,7 +80,7 @@ function update(req, res, next) {
|
||||
if (req.user.id === req.resource.id) return next(new HttpError(409, 'Cannot set active flag on self'));
|
||||
}
|
||||
|
||||
if (users.compareRoles(req.user.role, req.body.role) < 0) return next(new HttpError(403, `role '${req.body.role}' is required but you are only '${req.user.role}'`));
|
||||
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but you are only '${req.user.role}'`));
|
||||
|
||||
users.update(req.resource, req.body, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
+51
-40
@@ -30,7 +30,7 @@ function initializeExpressSync() {
|
||||
var QUERY_LIMIT = '1mb', // max size for json and urlencoded queries (see also client_max_body_size in nginx)
|
||||
FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart
|
||||
|
||||
var REQUEST_TIMEOUT = 10000; // timeout for all requests (see also setTimeout on the httpServer)
|
||||
var REQUEST_TIMEOUT = 20000; // timeout for all requests (see also setTimeout on the httpServer)
|
||||
|
||||
var json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
|
||||
urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded
|
||||
@@ -79,6 +79,7 @@ function initializeExpressSync() {
|
||||
// to keep routes code short
|
||||
const password = routes.accesscontrol.passwordAuth;
|
||||
const token = routes.accesscontrol.tokenAuth;
|
||||
const authorizeOwner = routes.accesscontrol.authorize(users.ROLE_OWNER);
|
||||
const authorizeAdmin = routes.accesscontrol.authorize(users.ROLE_ADMIN);
|
||||
const authorizeUserManager = routes.accesscontrol.authorize(users.ROLE_USER_MANAGER);
|
||||
|
||||
@@ -88,7 +89,7 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/cloudron/activate', routes.provision.activate);
|
||||
router.get ('/api/v1/cloudron/status', routes.provision.getStatus);
|
||||
|
||||
router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar
|
||||
router.get ('/api/v1/cloudron/avatar', routes.branding.getCloudronAvatar); // this is a public alias for /api/v1/branding/cloudron_avatar
|
||||
|
||||
// login/logout routes
|
||||
router.post('/api/v1/cloudron/login', password, routes.cloudron.login);
|
||||
@@ -190,58 +191,68 @@ 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) => {
|
||||
return req.params.setting === 'cloudron_avatar' ? multipart(req, res, next) : next();
|
||||
}, routes.branding.set);
|
||||
|
||||
// settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above)
|
||||
router.get ('/api/v1/settings/:setting', token, authorizeAdmin, routes.settings.get);
|
||||
router.post('/api/v1/settings/backup_config', token, authorizeOwner, routes.settings.setBackupConfig);
|
||||
router.post('/api/v1/settings/:setting', token, authorizeAdmin, (req, res, next) => {
|
||||
return req.params.setting === 'cloudron_avatar' ? multipart(req, res, next) : next();
|
||||
}, routes.settings.set);
|
||||
|
||||
// email routes
|
||||
router.get('/api/v1/mailserver/:pathname', token, authorizeAdmin, routes.mailserver.proxy);
|
||||
router.get('/api/v1/mailserver/:pathname', token, (req, res, next) => {
|
||||
// some routes are more special than others
|
||||
if (req.params.pathname === 'eventlog' || req.params.pathname === 'clear_eventlog') {
|
||||
return authorizeOwner(req, res, next);
|
||||
}
|
||||
authorizeAdmin(req, res, next);
|
||||
}, 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);
|
||||
|
||||
@@ -51,6 +51,8 @@ exports = module.exports = {
|
||||
setFooter: setFooter,
|
||||
|
||||
getAppstoreListingConfig: getAppstoreListingConfig,
|
||||
setAppstoreListingConfig: setAppstoreListingConfig,
|
||||
|
||||
getSupportConfig: getSupportConfig,
|
||||
|
||||
provider: provider,
|
||||
@@ -577,6 +579,20 @@ function getAppstoreListingConfig(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setAppstoreListingConfig(listingConfig, callback) {
|
||||
assert.strictEqual(typeof listingConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.set(exports.APPSTORE_LISTING_CONFIG_KEY, JSON.stringify(listingConfig), function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
notifyChange(exports.APPSTORE_LISTING_CONFIG_KEY, listingConfig);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getSupportConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
+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 * * *'),
|
||||
|
||||
+15
-15
@@ -18,6 +18,7 @@ var async = require('async'),
|
||||
mailboxdb = require('../mailboxdb.js'),
|
||||
maildb = require('../maildb.js'),
|
||||
mailer = require('../mailer.js'),
|
||||
paths = require('../paths.js'),
|
||||
settings = require('../settings.js'),
|
||||
userdb = require('../userdb.js'),
|
||||
users = require('../users.js'),
|
||||
@@ -73,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);
|
||||
}
|
||||
|
||||
@@ -283,10 +283,10 @@ describe('User', function () {
|
||||
it('fails for ghost with wrong password', function (done) {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
fs.writeFileSync(paths.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
users.verify(userObject.id, 'foobar', users.AP_WEBADMIN, function (error) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
fs.unlinkSync(paths.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.be.a(BoxError);
|
||||
expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS);
|
||||
@@ -297,10 +297,10 @@ describe('User', function () {
|
||||
it('succeeds for ghost', function (done) {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
fs.writeFileSync(paths.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
users.verify(userObject.id, 'testpassword', users.AP_WEBADMIN, function (error, result) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
fs.unlinkSync(paths.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.equal(null);
|
||||
expect(result.id).to.equal(userObject.id);
|
||||
@@ -315,10 +315,10 @@ describe('User', function () {
|
||||
it('succeeds for normal user password when ghost file exists', function (done) {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
fs.writeFileSync(paths.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
users.verify(userObject.id, PASSWORD, users.AP_WEBADMIN, function (error, result) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
fs.unlinkSync(paths.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
@@ -385,10 +385,10 @@ describe('User', function () {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
fs.writeFileSync(paths.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
users.verifyWithUsername(USERNAME, 'foobar', users.AP_WEBADMIN, function (error) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
fs.unlinkSync(paths.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.be.a(BoxError);
|
||||
expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS);
|
||||
@@ -400,10 +400,10 @@ describe('User', function () {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
fs.writeFileSync(paths.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
users.verifyWithUsername(USERNAME, 'testpassword', users.AP_WEBADMIN, function (error, result) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
fs.unlinkSync(paths.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.equal(null);
|
||||
expect(result.id).to.equal(userObject.id);
|
||||
@@ -472,10 +472,10 @@ describe('User', function () {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
fs.writeFileSync(paths.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
users.verifyWithEmail(EMAIL, 'foobar', users.AP_WEBADMIN, function (error) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
fs.unlinkSync(paths.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.be.a(BoxError);
|
||||
expect(error.reason).to.equal(BoxError.INVALID_CREDENTIALS);
|
||||
@@ -487,10 +487,10 @@ describe('User', function () {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
fs.writeFileSync(paths.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
users.verifyWithEmail(EMAIL, 'testpassword', users.AP_WEBADMIN, function (error, result) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
fs.unlinkSync(paths.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.equal(null);
|
||||
expect(result.id).to.equal(userObject.id);
|
||||
|
||||
@@ -82,6 +82,7 @@ function checkAppUpdates(callback) {
|
||||
|
||||
async.eachSeries(result, function (app, iteratorDone) {
|
||||
if (app.appStoreId === '') return iteratorDone(); // appStoreId can be '' for dev apps
|
||||
if (app.runState === apps.RSTATE_STOPPED) return iteratorDone(); // stopped apps won't run migration scripts and shouldn't be updated
|
||||
|
||||
appstore.getAppUpdate(app, function (error, updateInfo) {
|
||||
if (error) {
|
||||
|
||||
+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(',');
|
||||
|
||||
|
||||
+6
-5
@@ -31,7 +31,6 @@ exports = module.exports = {
|
||||
count: count,
|
||||
|
||||
AP_MAIL: 'mail',
|
||||
AP_SFTP: 'sftp',
|
||||
AP_WEBADMIN: 'webadmin',
|
||||
|
||||
ROLE_ADMIN: 'admin',
|
||||
@@ -58,6 +57,7 @@ let assert = require('assert'),
|
||||
groups = require('./groups.js'),
|
||||
hat = require('./hat.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
paths = require('./paths.js'),
|
||||
qrcode = require('qrcode'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
@@ -178,7 +178,7 @@ function create(username, password, email, displayName, options, auditSource, ca
|
||||
id: 'uid-' + uuid.v4(),
|
||||
username: username,
|
||||
email: email,
|
||||
fallbackEmail: email, // for new users the fallbackEmail is also the default email
|
||||
fallbackEmail: email,
|
||||
password: Buffer.from(derivedKey, 'binary').toString('hex'),
|
||||
salt: salt.toString('hex'),
|
||||
createdAt: now,
|
||||
@@ -209,7 +209,7 @@ function verifyGhost(username, password) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
var ghostData = safe.JSON.parse(safe.fs.readFileSync(constants.GHOST_USER_FILE, 'utf8'));
|
||||
var ghostData = safe.JSON.parse(safe.fs.readFileSync(paths.GHOST_USER_FILE, 'utf8'));
|
||||
if (!ghostData) return false;
|
||||
|
||||
if (username in ghostData && ghostData[username] === password) {
|
||||
@@ -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