Compare commits

..

6 Commits

Author SHA1 Message Date
Girish Ramakrishnan 77785097c1 5.3.4 changes 2020-07-03 14:22:45 -07:00
Girish Ramakrishnan 4991982770 5.3.3 changes
(cherry picked from commit bc6e652293)
2020-07-03 14:22:04 -07:00
Girish Ramakrishnan f4407f3a43 Fixes for tests
(cherry picked from commit 1c96fbb533)
2020-07-03 14:08:02 -07:00
Girish Ramakrishnan 56a82ef808 database: rework connection logic
(cherry picked from commit 3dc163c33d)
2020-07-03 14:07:54 -07:00
Girish Ramakrishnan ecce897b5a Fix crash when mysql crashes
(cherry picked from commit d1ff8e9d6b)
2020-07-03 09:58:33 -07:00
Girish Ramakrishnan c5c8b1e299 database: Fix event emitter warning
the connection object gets reused after release. this means that we keep
attaching the 'error' event and not unlistening.

--trace-warnings can be added to box.service to get the stack trace

(cherry picked from commit 70743bd285)
2020-07-03 09:58:27 -07:00
190 changed files with 4863 additions and 7884 deletions
-173
View File
@@ -2013,176 +2013,3 @@
[5.3.4]
* Fix issue in database error handling
[5.4.0]
* Update nginx to 1.18 for various security fixes
* Add ping capability (for statping app)
* Fix bug where aliases were displayed incorrectly in SOGo
* Add univention as LDAP provider
* Bump max_connection for postgres addon to 200
* mail: Add pagination to mailing list API
* Allow admin to lock email and display name of users
* Allow admin to ensure all users have 2FA setup
* ami: fix regression where we didn't send provider as part of get status call
* nginx: hide version
* backups: add b2 provider
* Add filemanager webinterface
* Add darkmode
* Add note that password reset and invite links expire in 24 hours
[5.4.1]
* Update nginx to 1.18 for various security fixes
* Add ping capability (for statping app)
* Fix bug where aliases were displayed incorrectly in SOGo
* Add univention as LDAP provider
* Bump max_connection for postgres addon to 200
* mail: Add pagination to mailing list API
* Allow admin to lock email and display name of users
* Allow admin to ensure all users have 2FA setup
* ami: fix regression where we didn't send provider as part of get status call
* nginx: hide version
* backups: add b2 provider
* Add filemanager webinterface
* Add darkmode
* Add note that password reset and invite links expire in 24 hours
[5.5.0]
* postgresql: update to PostgreSQL 11
* postgresql: add citext extension to whitelist for loomio
* postgresql: add btree_gist,postgres_fdw,pg_stat_statements,plpgsql extensions for gitlab
* SFTP/Filebrowser: fix access of external data directories
* Fix contrast issues in dark mode
* Add option to delete mailbox data when mailbox is delete
* Allow days/hours of backups and updates to be configurable
* backup cleaner: fix issue where referenced backups where not counted against time periods
* route53: fix issue where verification failed if user had more than 100 zones
* rework task workers to run them in a separate cgroup
* backups: now much faster thanks to reworking of task worker
* When custom fallback cert is set, make sure it's used over LE certs
* mongodb: update to MongoDB 4.0.19
* List groups ordered by name
* Invite links are now valid for a week
* Update release GPG key
* Add pre-defined variables ($CLOUDRON_APPID) for better post install messages
* filemanager: show folder first
[5.6.0]
* Remove IP nginx configuration that redirects to dashboard after activation
* dashboard: looks for search string in app title as well
* Add vaapi caps for transcoding
* Fix issue where the long mongodb database names where causing app indices of rocket.chat to overflow (> 127)
* Do not resize swap if swap file exists. This means that users can now control how swap is allocated on their own.
* SFTP: fix issue where parallel rebuilds would cause an error
* backups: make part size configurable
* mail: set max email size
* mail: allow mail server location to be set
* spamassassin: custom configs and wl/bl
* Do not automatically update to unstable release
* scheduler: reduce container churn
* mail: add API to set banner
* Fix bug where systemd 237 ignores --nice value in systemd-run
* postgresql: enable uuid-ossp extension
* firewall: add blocklist
* HTTP URLs now redirect directly to the HTTPS of the final domain
* linode: Add singapore region
* ovh: add sydney region
* s3: makes multi-part copies in parallel
[5.6.1]
* Blocklists are now stored in a text file instead of json
* regenerate nginx configs
[5.6.2]
* Update docker to 19.03.12
* Fix sorting of user listing in the UI
* namecheap: fix crash when server returns invalid response
* unlink ghost file automatically on successful login
* Bump mysql addon connection limit to 200
* Fix install issue where `/dev/dri` may not be present
* import: when importing filesystem backups, the input box is a path
* firewall: fix race condition where blocklist was not added in correct position in the FORWARD chain
* services: fix issue where services where scaled up/down too fast
* turn: realm variable was not updated properly on dashboard change
* nginx: add splash pages for IP based browser access
* Give services panel a separate top-level view
* Add app state filter
* gcs: copy concurrency was not used
* Mention why an app update cannot be applied and provide shortcut to start the app if stopped
* Remove version from footer into the setting view
* Give services panel a separate top-level view
* postgresql: set collation order explicity when creating database to C.UTF-8 (for confluence)
* rsync: fix error while goes missing when syncing
* Pre-select app domain by default in the redirection drop down
* robots: preseve leading and trailing whitespaces/newlines
[5.6.3]
* Fix postgres locale issue
[6.0.0]
* Focal support
* Reduce duration of self-signed certs to 800 days
* Better backup config filename when downloading
* branding: footer can have template variables like %YEAR% and %VERSION%
* sftp: secure the API with a token
* filemanager: Add extract context menu item
* Do not download docker images if present locally
* sftp: disable access to non-admins by default
* postgresql: whitelist pgcrypto extension for loomio
* filemanager: Add new file creation action and collapse new and upload actions
* rsync: add warning to remove lifecycle rules
* Add volume management
* backups: adjust node's heap size based on memory limit
* s3: diasble per-chunk timeout
* logs: more descriptive log file names on download
* collectd: remove collectd config when app stopped (and add it back when started)
* Apps can optionally request an authwall to be installed in front of them
* mailbox can now owned by a group
* linode: enable dns provider in setup view
* dns: apps can now use the dns port
* httpPaths: allow apps to specify forwarding from custom paths to container ports (for OLS)
* add elasticemail smtp relay option
* mail: add option to fts using solr
* mail: change the namespace separator of new installations to /
* mail: enable acl
* Disable THP
* filemanager: allow download dirs as zip files
* aws: add china region
* security: fix issue where apps could send with any username (but valid password)
* i18n support
[6.0.1]
* app: add export route
* mail: on location change, fix lock up when one or more domains have invalid credentials
* mail: fix crash because of write after timeout closure
* scaleway: fix installation issue where THP is not enabled in kernel
[6.1.0]
* mail: update haraka to 2.8.27. this fixes zero-length queue file crash
* update: set/unset appStoreId from the update route
* proxyauth: Do not follow redirects
* proxyauth: add 2FA
* appstore: add category translations
* appstore: add media category
* prepend the version to assets when sourcing to avoid cache hits on update
* filemanger: list volumes of the app
* Display upload size and size progress
* nfs: chown the backups for hardlinks to work
* remove user add/remove/role change email notifications
* persist update indicator across restarts
* cloudron-setup: add --generate-setup-token
* dashboard: pass accessToken query param to automatically login
* wellknown: add a way to set well known docs
* oom: notification mails have links to dashboard
* collectd: do not install xorg* packages
* apptask: backup/restore tasks now use the backup memory limit configuration
* eventlog: add logout event
* mailbox: include alias in mailbox search
* proxyAuth: add path exclusion
* turn: fix for CVE-2020-26262
* app password: fix regression where apps are not listed anymore in the UI
* Support for multiDomain apps (domain aliases)
* netcup: add dns provider
* Container swap size is now dynamically determined based on system RAM/swap ratio
[6.1.1]
* Fix bug where platform does not start if memory limits could not be applied
+4 -26
View File
@@ -29,9 +29,9 @@ anyone to effortlessly host web applications on their server on their own terms.
* Trivially migrate to another server keeping your apps and data (for example, switch your
infrastructure provider or move to a bigger server).
* Comprehensive [REST API](https://docs.cloudron.io/api/).
* Comprehensive [REST API](https://cloudron.io/documentation/developer/api/).
* [CLI](https://docs.cloudron.io/custom-apps/cli/) to configure apps.
* [CLI](https://cloudron.io/documentation/cli/) to configure apps.
* Alerts, audit logs, graphs, dns management ... and much more
@@ -41,37 +41,15 @@ Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudr
## Installing
[Install script](https://docs.cloudron.io/installation/) - [Pricing](https://cloudron.io/pricing.html)
[Install script](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
**Note:** This repo is a small part of what gets installed on your server - there is
the dashboard, database addons, graph container, base image etc. Cloudron also relies
on external services such as the App Store for apps to be installed. As such, don't
clone this repo and npm install and expect something to work.
## Development
This is the backend code of Cloudron. The frontend code is [here](https://git.cloudron.io/cloudron/dashboard).
The way to develop is to first install a full instance of Cloudron in a VM. Then you can use the [hotfix](https://git.cloudron.io/cloudron/cloudron-machine)
tool to patch the VM with the latest code.
```
SSH_PASSPHRASE=sshkeypassword cloudron-machine hotfix --cloudron my.example.com --release 6.0.0 --ssh-key keyname
```
## License
Please note that the Cloudron code is under a source-available license. This is not the same as an
open source license but ensures the code is available for introspection (and hacking!).
## Contributions
Just to give some heads up, we are a bit restrictive in merging changes. We are a small team and
would like to keep our maintenance burden low. It might be best to discuss features first in the [forum](https://forum.cloudron.io),
to also figure out how many other people will use it to justify maintenance for a feature.
## Support
* [Documentation](https://docs.cloudron.io/)
* [Documentation](https://cloudron.io/documentation/)
* [Forum](https://forum.cloudron.io/)
+26 -37
View File
@@ -13,9 +13,6 @@ function die {
export DEBIAN_FRONTEND=noninteractive
readonly ubuntu_codename=$(lsb_release -cs)
readonly ubuntu_version=$(lsb_release -rs)
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
apt-mark hold grub* >/dev/null
apt-get -o Dpkg::Options::="--force-confdef" update -y
@@ -29,12 +26,11 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
ubuntu_version=$(lsb_release -rs)
ubuntu_codename=$(lsb_release -cs)
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
mysql_package=$([[ "${ubuntu_version}" == "20.04" ]] && echo "mysql-server-8.0" || echo "mysql-server-5.7")
apt-get -y install --no-install-recommends \
apt-get -y install \
acl \
apparmor \
build-essential \
cifs-utils \
cron \
@@ -42,12 +38,11 @@ apt-get -y install --no-install-recommends \
debconf-utils \
dmsetup \
$gpg_package \
ipset \
iptables \
libpython2.7 \
linux-generic \
logrotate \
$mysql_package \
mysql-server-5.7 \
openssh-server \
pwgen \
resolvconf \
@@ -55,17 +50,21 @@ apt-get -y install --no-install-recommends \
tzdata \
unattended-upgrades \
unbound \
unzip \
xfsprogs
echo "==> installing nginx for xenial for TLSv3 support"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-2~${ubuntu_codename}_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
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 --no-install-recommends sudo
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
@@ -76,7 +75,7 @@ mkdir -p /usr/local/node-10.18.1
curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxf - --strip-components=1 -C /usr/local/node-10.18.1
ln -sf /usr/local/node-10.18.1/bin/node /usr/bin/node
ln -sf /usr/local/node-10.18.1/bin/npm /usr/bin/npm
apt-get install -y --no-install-recommends python # Install python which is required for npm rebuild
apt-get install -y python # Install python which is required for npm rebuild
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
# https://docs.docker.com/engine/installation/linux/ubuntulinux/
@@ -87,9 +86,9 @@ mkdir -p /etc/systemd/system/docker.service.d
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
# there are 3 packages for docker - containerd, CLI and the daemon
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.13-2_amd64.deb" -o /tmp/containerd.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
@@ -102,7 +101,7 @@ fi
# do not upgrade grub because it might prompt user and break this script
echo "==> Enable memory accounting"
apt-get -y --no-upgrade --no-install-recommends install grub2-common
apt-get -y --no-upgrade install grub2-common
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
update-grub
@@ -121,37 +120,26 @@ for image in ${images}; do
done
echo "==> Install collectd"
# without this, libnotify4 will install gnome-shell
apt-get install -y libnotify4 --no-install-recommends
if ! apt-get install -y --no-install-recommends libcurl3-gnutls 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
fi
# https://bugs.launchpad.net/ubuntu/+source/collectd/+bug/1872281
[[ "${ubuntu_version}" == "20.04" ]] && echo -e "\nLD_PRELOAD=/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so" >> /etc/default/collectd
# some hosts like atlantic install ntp which conflicts with timedatectl. https://serverfault.com/questions/1024770/ubuntu-20-04-time-sync-problems-and-possibly-incorrect-status-information
echo "==> Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
if systemctl is-active ntp; then
systemctl stop ntp
apt purge -y ntp
fi
timedatectl set-ntp 1
# mysql follows the system timezone
timedatectl set-timezone UTC
echo "==> Adding sshd configuration warning"
sed -e '/Port 22/ i # NOTE: Cloudron only supports moving SSH to port 202. See https://docs.cloudron.io/security/#securing-ssh-access' -i /etc/ssh/sshd_config
sed -e '/Port 22/ i # NOTE: Cloudron only supports moving SSH to port 202. See https://cloudron.io/documentation/security/#securing-ssh-access' -i /etc/ssh/sshd_config
# https://bugs.launchpad.net/ubuntu/+source/base-files/+bug/1701068
echo "==> Disabling motd news"
if [ -f "/etc/default/motd-news" ]; then
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/motd-news
fi
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/motd-news
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed)
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
systemctl stop bind9 || true
systemctl disable bind9 || true
@@ -163,7 +151,7 @@ systemctl disable dnsmasq || true
systemctl stop postfix || true
systemctl disable postfix || true
# on ubuntu 18.04 and 20.04, this is the default. this requires resolvconf for DNS to work further after the disable
# on ubuntu 18.04, this is the default. this requires resolvconf for DNS to work further after the disable
systemctl stop systemd-resolved || true
systemctl disable systemd-resolved || true
@@ -172,3 +160,4 @@ systemctl disable systemd-resolved || true
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: ${ip6}" > /etc/unbound/unbound.conf.d/cloudron-network.conf
systemctl restart unbound
+40 -48
View File
@@ -2,65 +2,57 @@
'use strict';
// prefix all output with a timestamp
// debug() already prefixes and uses process.stderr NOT console.*
['log', 'info', 'warn', 'debug', 'error'].forEach(function (log) {
var orig = console[log];
console[log] = function () {
orig.apply(console, [new Date().toISOString()].concat(Array.prototype.slice.call(arguments)));
};
});
require('supererror')({ splatchError: true });
let async = require('async'),
constants = require('./src/constants.js'),
dockerProxy = require('./src/dockerproxy.js'),
fs = require('fs'),
ldap = require('./src/ldap.js'),
paths = require('./src/paths.js'),
proxyAuth = require('./src/proxyauth.js'),
server = require('./src/server.js');
const NOOP_CALLBACK = function () { };
function setupLogging(callback) {
if (process.env.BOX_ENV === 'test') return callback();
var logfileStream = fs.createWriteStream(paths.BOX_LOG_FILE, { flags:'a' });
process.stdout.write = process.stderr.write = logfileStream.write.bind(logfileStream);
callback();
}
console.log();
console.log('==========================================');
console.log(` Cloudron ${constants.VERSION} `);
console.log('==========================================');
console.log();
async.series([
setupLogging,
server.start, // do this first since it also inits the database
proxyAuth.start,
server.start,
ldap.start,
dockerProxy.start
], function (error) {
if (error) {
console.log('Error starting server', error);
console.error('Error starting server', error);
process.exit(1);
}
// require those here so that logging handler is already setup
require('supererror');
const debug = require('debug')('box:box');
process.on('SIGINT', function () {
debug('Received SIGINT. Shutting down.');
proxyAuth.stop(NOOP_CALLBACK);
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
debug('Received SIGTERM. Shutting down.');
proxyAuth.stop(NOOP_CALLBACK);
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('uncaughtException', function (error) {
console.error((error && error.stack) ? error.stack : error);
setTimeout(process.exit.bind(process, 1), 3000);
});
console.log(`Cloudron is up and running. Logs are at ${paths.BOX_LOG_FILE}`); // this goes to journalctl
console.log('Cloudron is up and running');
});
var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () {
console.log('Received SIGINT. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
console.log('Received SIGTERM. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
@@ -1,16 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
db.runSql('ALTER TABLE users DROP COLUMN modifiedAt', callback);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN ts', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,29 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
if (backupConfig.intervalSecs === 6 * 60 * 60) { // every 6 hours
backupConfig.schedulePattern = '00 00 5,11,17,23 * * *';
} else if (backupConfig.intervalSecs === 12 * 60 * 60) { // every 12 hours
backupConfig.schedulePattern = '00 00 5,17 * * *';
} else if (backupConfig.intervalSecs === 24 * 60 * 60) { // every day
backupConfig.schedulePattern = '00 00 23 * * *';
} else if (backupConfig.intervalSecs === 3 * 24 * 60 * 60) { // every 3 days (based on day)
backupConfig.schedulePattern = '00 00 23 * * 1,3,5';
} else if (backupConfig.intervalSecs === 7 * 24 * 60 * 60) { // every week (saturday)
backupConfig.schedulePattern = '00 00 23 * * 6';
} else { // default to everyday
backupConfig.schedulePattern = '00 00 23 * * *';
}
delete backupConfig.intervalSecs;
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,23 +0,0 @@
'use strict';
const async = require('async');
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="admin_domain"', function (error, results) {
if (error || results.length === 0) return callback(error);
const adminDomain = results[0].value;
async.series([
db.runSql.bind(db, 'INSERT INTO settings (name, value) VALUES (?, ?)', [ 'mail_domain', adminDomain ]),
db.runSql.bind(db, 'INSERT INTO settings (name, value) VALUES (?, ?)', [ 'mail_fqdn', `my.${adminDomain}` ])
], callback);
});
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'DELETE FROM settings WHERE name="mail_domain"'),
db.runSql.bind(db, 'DELETE FROM settings WHERE name="mail_fqdn"'),
], callback);
};
@@ -1,22 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM settings WHERE name=?', ['app_autoupdate_pattern'], function (error, results) {
if (error || results.length === 0) return callback(error); // will use defaults from box code
var updatePattern = results[0].value; // use app auto update patter for the box as well
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'DELETE FROM settings WHERE name=? OR name=?', ['app_autoupdate_pattern', 'box_autoupdate_pattern']),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['autoupdate_pattern', updatePattern]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mail ADD COLUMN bannerJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mail DROP COLUMN bannerJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,27 +0,0 @@
'use strict';
const OLD_FIREWALL_CONFIG_JSON = '/home/yellowtent/boxdata/firewall-config.json';
const PORTS_FILE = '/home/yellowtent/boxdata/firewall/ports.json';
const BLOCKLIST_FILE = '/home/yellowtent/boxdata/firewall/blocklist.txt';
const fs = require('fs');
exports.up = function (db, callback) {
if (!fs.existsSync(OLD_FIREWALL_CONFIG_JSON)) return callback();
try {
const dataJson = fs.readFileSync(OLD_FIREWALL_CONFIG_JSON, 'utf8');
const data = JSON.parse(dataJson);
fs.writeFileSync(BLOCKLIST_FILE, data.blocklist.join('\n') + '\n', 'utf8');
fs.writeFileSync(PORTS_FILE, JSON.stringify({ allowed_tcp_ports: data.allowed_tcp_ports }, null, 4), 'utf8');
fs.unlinkSync(OLD_FIREWALL_CONFIG_JSON);
} catch (error) {
console.log('Error migrating old firewall config', error);
}
callback();
};
exports.down = function (db, callback) {
callback();
};
@@ -1,40 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd1 = 'CREATE TABLE volumes(' +
'id VARCHAR(128) NOT NULL UNIQUE,' +
'name VARCHAR(256) NOT NULL UNIQUE,' +
'hostPath VARCHAR(1024) NOT NULL UNIQUE,' +
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'PRIMARY KEY (id)) CHARACTER SET utf8 COLLATE utf8_bin';
var cmd2 = 'CREATE TABLE appMounts(' +
'appId VARCHAR(128) NOT NULL,' +
'volumeId VARCHAR(128) NOT NULL,' +
'readOnly BOOLEAN DEFAULT 1,' +
'UNIQUE KEY appMounts_appId_volumeId (appId, volumeId),' +
'FOREIGN KEY(appId) REFERENCES apps(id),' +
'FOREIGN KEY(volumeId) REFERENCES volumes(id)) CHARACTER SET utf8 COLLATE utf8_bin;';
db.runSql(cmd1, function (error) {
if (error) console.error(error);
db.runSql(cmd2, function (error) {
if (error) console.error(error);
db.runSql('ALTER TABLE apps DROP COLUMN bindsJson', callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE appMounts', function (error) {
if (error) console.error(error);
db.runSql('DROP TABLE volumes', function (error) {
if (error) console.error(error);
callback(error);
});
});
};
@@ -1,16 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN proxyAuth BOOLEAN DEFAULT 0', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN proxyAuth', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,18 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN ownerType VARCHAR(16)'),
db.runSql.bind(db, 'UPDATE mailboxes SET ownerType=?', [ 'user' ]),
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY ownerType VARCHAR(16) NOT NULL'),
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN ownerType', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,13 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN httpPort')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,29 +0,0 @@
'use strict';
const async = require('async'),
iputils = require('../src/iputils.js');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN containerIp VARCHAR(16) UNIQUE', function (error) {
if (error) console.error(error);
let baseIp = iputils.intFromIp('172.18.16.0');
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, iteratorDone) {
const nextIp = iputils.ipFromInt(++baseIp);
db.runSql('UPDATE apps SET containerIp=? WHERE id=?', [ nextIp, app.id ], iteratorDone);
}, callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN containerIp', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,21 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT * FROM settings WHERE name=?', ['platform_config'], function (error, results) {
let value;
if (error || results.length === 0) {
value = { sftp: { requireAdmin: true } };
} else {
value = JSON.parse(results[0].value);
if (!value.sftp) value.sftp = {};
value.sftp.requireAdmin = true;
}
// existing installations may not even have the key. so use REPLACE instead of UPDATE
db.runSql('REPLACE INTO settings (name, value) VALUES (?, ?)', [ 'platform_config', JSON.stringify(value) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,18 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'CREATE TABLE groupMembers_copy(groupId VARCHAR(128) NOT NULL, userId VARCHAR(128) NOT NULL, FOREIGN KEY(groupId) REFERENCES userGroups(id), FOREIGN KEY(userId) REFERENCES users(id), UNIQUE (groupId, userId)) CHARACTER SET utf8 COLLATE utf8_bin'), // In mysql CREATE TABLE.. LIKE does not copy indexes
db.runSql.bind(db, 'INSERT INTO groupMembers_copy SELECT * FROM groupMembers GROUP BY groupId, userId'),
db.runSql.bind(db, 'DROP TABLE groupMembers'),
db.runSql.bind(db, 'ALTER TABLE groupMembers_copy RENAME TO groupMembers')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE groupMembers DROP INDEX groupMembers_member'),
], callback);
};
@@ -1,51 +0,0 @@
'use strict';
const async = require('async'),
safe = require('safetydance');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE domains ADD COLUMN wellKnownJson TEXT', function (error) {
if (error) return callback(error);
// keep the paths around, so that we don't need to trigger a re-configure. the old nginx config will use the paths
// the new one will proxy calls to the box code
const WELLKNOWN_DIR = '/home/yellowtent/boxdata/well-known';
const output = safe.child_process.execSync('find . -type f -printf "%P\n"', { cwd: WELLKNOWN_DIR, encoding: 'utf8' });
if (!output) return callback();
const paths = output.trim().split('\n');
if (paths.length === 0) return callback(); // user didn't configure any well-known
let wellKnown = {};
for (let path of paths) {
const fqdn = path.split('/', 1)[0];
const loc = path.slice(fqdn.length+1);
const doc = safe.fs.readFileSync(`${WELLKNOWN_DIR}/${path}`, { encoding: 'utf8' });
if (!doc) continue;
wellKnown[fqdn] = {};
wellKnown[fqdn][loc] = doc;
}
console.log('Migrating well-known', JSON.stringify(wellKnown, null, 4));
async.eachSeries(Object.keys(wellKnown), function (fqdn, iteratorDone) {
db.runSql('UPDATE domains SET wellKnownJson=? WHERE domain=?', [ JSON.stringify(wellKnown[fqdn]), fqdn ], function (error, result) {
if (error) {
console.error(error); // maybe the domain does not exist anymore
} else if (result.affectedRows === 0) {
console.log(`Could not migrate wellknown as domain ${fqdn} is missing`);
}
iteratorDone();
});
}, function (error) {
callback(error);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE domains DROP COLUMN wellKnownJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,23 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT * FROM settings WHERE name=?', ['platform_config'], function (error, results) {
if (error || results.length === 0) return callback(null);
let value = JSON.parse(results[0].value);
for (const serviceName of Object.keys(value)) {
const service = value[serviceName];
if (!service.memorySwap) continue;
service.memoryLimit = service.memorySwap;
delete service.memorySwap;
delete service.memory;
}
db.runSql('UPDATE settings SET value=? WHERE name=?', [ JSON.stringify(value), 'platform_config' ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,28 +0,0 @@
'use strict';
const async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, iteratorDone) {
if (!app.servicesConfigJson) return iteratorDone();
let servicesConfig = JSON.parse(app.servicesConfigJson);
for (const serviceName of Object.keys(servicesConfig)) {
const service = servicesConfig[serviceName];
if (!service.memorySwap) continue;
service.memoryLimit = service.memorySwap;
delete service.memorySwap;
delete service.memory;
}
db.runSql('UPDATE apps SET servicesConfigJson=? WHERE id=?', [ JSON.stringify(servicesConfig), app.id ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,9 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE settings SET name=? WHERE name=?', [ 'services_config', 'platform_config' ], callback);
};
exports.down = function(db, callback) {
db.runSql('UPDATE settings SET name=? WHERE name=?', [ 'platform_config', 'services_config' ], callback);
};
+6 -24
View File
@@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS users(
password VARCHAR(1024) NOT NULL,
salt VARCHAR(512) NOT NULL,
createdAt VARCHAR(512) NOT NULL,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
modifiedAt VARCHAR(512) NOT NULL,
displayName VARCHAR(512) DEFAULT "",
fallbackEmail VARCHAR(512) DEFAULT "",
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
@@ -44,8 +44,7 @@ CREATE TABLE IF NOT EXISTS groupMembers(
groupId VARCHAR(128) NOT NULL,
userId VARCHAR(128) NOT NULL,
FOREIGN KEY(groupId) REFERENCES userGroups(id),
FOREIGN KEY(userId) REFERENCES users(id),
UNIQUE (groupId, userId));
FOREIGN KEY(userId) REFERENCES users(id));
CREATE TABLE IF NOT EXISTS tokens(
id VARCHAR(128) NOT NULL UNIQUE,
@@ -66,6 +65,9 @@ CREATE TABLE IF NOT EXISTS apps(
healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded
containerId VARCHAR(128),
manifestJson TEXT,
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
@@ -85,8 +87,7 @@ CREATE TABLE IF NOT EXISTS apps(
dataDir VARCHAR(256) UNIQUE,
taskId INTEGER, // current task
errorJson TEXT,
servicesConfigJson TEXT, // app services configuration
containerIp VARCHAR(16) UNIQUE, // this is not-null because of ip allocation fails, user can 'repair'
bindsJson TEXT, // bind mounts
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(taskId) REFERENCES tasks(id),
@@ -148,7 +149,6 @@ CREATE TABLE IF NOT EXISTS domains(
provider VARCHAR(16) NOT NULL,
configJson TEXT, /* JSON containing the dns backend provider config */
tlsConfigJson TEXT, /* JSON containing the tls provider config */
wellKnownJson TEXT, /* JSON containing well known docs for this domain */
PRIMARY KEY (domain))
@@ -162,7 +162,6 @@ CREATE TABLE IF NOT EXISTS mail(
mailFromValidation BOOLEAN DEFAULT 1,
catchAllJson TEXT,
relayJson TEXT,
bannerJson TEXT,
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
@@ -182,7 +181,6 @@ CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* user id */
ownerType VARCHAR(16) NOT NULL,
aliasName VARCHAR(128), /* the target name type is an alias */
aliasDomain VARCHAR(128), /* the target domain */
membersJson TEXT, /* members of a group. fully qualified */
@@ -239,20 +237,4 @@ CREATE TABLE IF NOT EXISTS appPasswords(
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS volumes(
id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(256) NOT NULL UNIQUE,
hostPath VARCHAR(1024) NOT NULL UNIQUE,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS appMounts(
appId VARCHAR(128) NOT NULL,
volumeId VARCHAR(128) NOT NULL,
readOnly BOOLEAN DEFAULT 1,
UNIQUE KEY appMounts_appId_volumeId (appId, volumeId),
FOREIGN KEY(appId) REFERENCES apps(id),
FOREIGN KEY(volumeId) REFERENCES volumes(id));
CHARACTER SET utf8 COLLATE utf8_bin;
+380 -474
View File
File diff suppressed because it is too large Load Diff
+22 -26
View File
@@ -14,49 +14,45 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^1.2.9",
"@google-cloud/dns": "^1.1.0",
"@google-cloud/storage": "^2.5.0",
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
"async": "^2.6.3",
"aws-sdk": "^2.828.0",
"basic-auth": "^2.0.1",
"aws-sdk": "^2.685.0",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^5.10.1",
"cloudron-manifestformat": "^5.4.0",
"connect": "^3.7.0",
"connect-lastmile": "^2.0.0",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.4.5",
"cookie-session": "^1.4.0",
"cron": "^1.8.2",
"db-migrate": "^0.11.12",
"db-migrate-mysql": "^2.1.2",
"debug": "^4.3.1",
"db-migrate": "^0.11.11",
"db-migrate-mysql": "^1.1.10",
"debug": "^4.1.1",
"dockerode": "^2.5.8",
"ejs": "^2.6.1",
"ejs-cli": "^2.2.1",
"ejs-cli": "^2.2.0",
"express": "^4.17.1",
"ipaddr.js": "^2.0.0",
"js-yaml": "^3.14.0",
"json": "^9.0.6",
"jsonwebtoken": "^8.5.1",
"ldapjs": "^2.2.3",
"lodash": "^4.17.20",
"ldapjs": "^1.0.2",
"lodash": "^4.17.15",
"lodash.chunk": "^4.2.0",
"mime": "^2.5.0",
"moment": "^2.29.1",
"moment-timezone": "^0.5.32",
"mime": "^2.4.6",
"moment": "^2.26.0",
"moment-timezone": "^0.5.31",
"morgan": "^1.10.0",
"multiparty": "^4.2.2",
"mustache-express": "^1.3.0",
"multiparty": "^4.2.1",
"mysql": "^2.18.1",
"nodemailer": "^6.4.17",
"nodemailer": "^6.4.6",
"nodemailer-smtp-transport": "^2.7.4",
"once": "^1.4.0",
"pretty-bytes": "^5.5.0",
"parse-links": "^0.1.0",
"pretty-bytes": "^5.3.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.4.4",
"readdirp": "^3.5.0",
"readdirp": "^3.4.0",
"request": "^2.88.2",
"rimraf": "^2.6.3",
"s3-block-read-stream": "^0.5.0",
@@ -65,22 +61,22 @@
"showdown": "^1.9.1",
"speakeasy": "^2.0.0",
"split": "^1.0.1",
"superagent": "^5.3.1",
"superagent": "^5.2.2",
"supererror": "^0.7.2",
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
"tar-stream": "^2.2.0",
"tar-stream": "^2.1.2",
"tldjs": "^2.3.1",
"underscore": "^1.12.0",
"underscore": "^1.10.2",
"uuid": "^3.4.0",
"validator": "^11.0.0",
"ws": "^7.4.2",
"ws": "^7.3.0",
"xml2js": "^0.4.23"
},
"devDependencies": {
"expect.js": "*",
"hock": "^1.4.1",
"js2xmlparser": "^4.0.1",
"mocha": "^6.2.3",
"mocha": "^6.1.4",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^10.0.6",
"node-sass": "^4.14.1",
+6 -14
View File
@@ -2,11 +2,11 @@
set -eu
readonly source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SOURCE_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly DATA_DIR="${HOME}/.cloudron_test"
readonly DEFAULT_TESTS="./src/test/*-test.js ./src/routes/test/*-test.js"
! "${source_dir}/src/test/checkInstall" && exit 1
! "${SOURCE_dir}/src/test/checkInstall" && exit 1
# cleanup old data dirs some of those docker container data requires sudo to be removed
echo "=> Provide root password to purge any leftover data in ${DATA_DIR} and load apparmor profile:"
@@ -22,27 +22,19 @@ fi
mkdir -p ${DATA_DIR}
cd ${DATA_DIR}
mkdir -p appsdata
mkdir -p boxdata/profileicons boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com boxdata/sftp/ssh
mkdir -p platformdata/addons/mail/banner platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
sudo mkdir -p /mnt/cloudron-test-music /media/cloudron-test-music # volume test
# translations
mkdir -p box/dashboard/dist/translation
cp -r ${source_dir}/../dashboard/dist/translation/* box/dashboard/dist/translation
mkdir -p boxdata/profileicons boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
mkdir -p platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
# put cert
echo "=> Generating a localhost selfsigned cert"
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.localhost"))
# generate legacy key format for sftp
ssh-keygen -m PEM -t rsa -f boxdata/sftp/ssh/ssh_host_rsa_key -q -N ""
# clear out any containers
echo "=> Delete all docker containers first"
docker ps -qa | xargs --no-run-if-empty docker rm -f
# create docker network (while the infra code does this, most tests skip infra setup)
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
docker network create --subnet=172.18.0.0/16 cloudron || true
# create the same mysql server version to test with
OUT=`docker inspect mysql-server` || true
@@ -67,7 +59,7 @@ echo "=> Ensure database"
mysql -h"${MYSQL_IP}" -uroot -ppassword -e 'CREATE DATABASE IF NOT EXISTS box'
echo "=> Run database migrations"
cd "${source_dir}"
cd "${SOURCE_dir}"
BOX_ENV=test DATABASE_URL=mysql://root:password@${MYSQL_IP}/box node_modules/.bin/db-migrate up
echo "=> Run tests with mocha"
+19 -20
View File
@@ -43,37 +43,30 @@ fi
initBaseImage="true"
provider="generic"
requestedVersion=""
installServerOrigin="https://api.cloudron.io"
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
sourceTarballUrl=""
rebootServer="true"
setupToken=""
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot,generate-setup-token" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--help) echo "See https://docs.cloudron.io/installation/ on how to install Cloudron"; exit 0;;
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
--provider) provider="$2"; shift 2;;
--version) requestedVersion="$2"; shift 2;;
--env)
if [[ "$2" == "dev" ]]; then
apiServerOrigin="https://api.dev.cloudron.io"
webServerOrigin="https://dev.cloudron.io"
installServerOrigin="https://api.dev.cloudron.io"
elif [[ "$2" == "staging" ]]; then
apiServerOrigin="https://api.staging.cloudron.io"
webServerOrigin="https://staging.cloudron.io"
installServerOrigin="https://api.staging.cloudron.io"
elif [[ "$2" == "unstable" ]]; then
installServerOrigin="https://api.dev.cloudron.io"
fi
shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--generate-setup-token) setupToken="$(openssl rand -hex 10)"; shift;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
@@ -87,8 +80,8 @@ fi
# Only --help works with mismatched ubuntu
ubuntu_version=$(lsb_release -rs)
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" && "${ubuntu_version}" != "20.04" ]]; then
echo "Cloudron requires Ubuntu 16.04, 18.04 or 20.04" > /dev/stderr
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" ]]; then
echo "Cloudron requires Ubuntu 16.04 or 18.04" > /dev/stderr
exit 1
fi
@@ -107,20 +100,32 @@ echo " Join us at https://forum.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
echo "=> Installing software-properties-common"
if ! apt-get install -y software-properties-common &>> "${LOG_FILE}"; then
echo "Could not install software-properties-common (for add-apt-repository below). See ${LOG_FILE}"
exit 1
fi
echo "=> Ensure required apt sources"
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
exit 1
fi
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories. See ${LOG_FILE}"
exit 1
fi
if ! DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y install --no-install-recommends curl python3 ubuntu-standard software-properties-common -y &>> "${LOG_FILE}"; then
if ! DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
exit 1
fi
fi
echo "=> Checking version"
if ! releaseJson=$($curl -s "${installServerOrigin}/api/v1/releases?boxVersion=${requestedVersion}"); then
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
@@ -158,7 +163,6 @@ fi
echo "=> Installing version ${version} (this takes some time) ..."
mkdir -p /etc/cloudron
echo "${provider}" > /etc/cloudron/PROVIDER
[[ ! -z "${setupToken}" ]] && echo "${setupToken}" > /etc/cloudron/SETUP_TOKEN
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
@@ -180,12 +184,7 @@ done
if ! ip=$(curl -s --fail --connect-timeout 2 --max-time 2 https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
ip='<IP>'
fi
if [[ -z "${setupToken}" ]]; then
url="https://${ip}"
else
url="https://${ip}/?setupToken=${setupToken}"
fi
echo -e "\n\n${GREEN}After reboot, visit ${url} and accept the self-signed certificate to finish setup.${DONE}\n"
echo -e "\n\n${GREEN}Visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n"
if [[ "${rebootServer}" == "true" ]]; then
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
+4 -4
View File
@@ -37,12 +37,12 @@ while true; do
# fall through
;&
--owner-login)
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' AND username IS NOT NULL ORDER BY createdAt LIMIT 1" 2>/dev/null)
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' LIMIT 1" 2>/dev/null)
admin_password=$(pwgen -1s 12)
ghost_file=/home/yellowtent/platformdata/cloudron_ghost.json
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} . This password may only be used once. ${ghost_file} will be automatically removed after use."
echo "Login as ${admin_username} / ${admin_password} . Remove ${ghost_file} when done."
exit 0
;;
--) break;;
@@ -57,7 +57,7 @@ if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
echo ""
df -h
echo ""
echo "To recover from a full disk, follow the guide at https://docs.cloudron.io/troubleshooting/#recovery-after-disk-full"
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/troubleshooting/#recovery-after-disk-full"
exit 1
fi
@@ -94,7 +94,7 @@ echo -e $LINE"Backup stats (possibly misleading)"$LINE >> $OUT
du -hcsL /var/backups/* &>> $OUT || true
echo -e $LINE"System daemon status"$LINE >> $OUT
systemctl status --lines=100 box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
echo -e $LINE"Box logs"$LINE >> $OUT
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
-31
View File
@@ -1,31 +0,0 @@
#!/bin/bash
set -eu -o pipefail
# This script downloads new translation data from weblate at https://translate.cloudron.io
OUT="/home/yellowtent/box/dashboard/dist/translation"
# We require root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root. Run with sudo"
exit 1
fi
echo "=> Downloading new translation files..."
curl https://translate.cloudron.io/download/cloudron/dashboard/?format=zip -o /tmp/lang.zip
echo "=> Unpacking..."
unzip -jo /tmp/lang.zip -d $OUT
chown -R yellowtent:yellowtent $OUT
# unzip put very restrictive permissions
chmod ua+r $OUT/*
echo "=> Cleanup..."
rm /tmp/lang.zip
echo "=> Done"
echo ""
echo "Reload the dashboard to see the new translations"
echo ""
+9 -13
View File
@@ -27,11 +27,11 @@ echo "==> installer: Updating from $(cat $box_src_dir/VERSION) to $(cat $box_src
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "19.03.12" ]]; then
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
# there are 3 packages for docker - containerd, CLI and the daemon
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.13-2_amd64.deb" -o /tmp/containerd.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
echo "==> installer: Waiting for all dpkg tasks to finish..."
while fuser /var/lib/dpkg/lock; do
@@ -57,19 +57,15 @@ if [[ $(docker version --format {{.Client.Version}}) != "19.03.12" ]]; then
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
fi
readonly nginx_version=$(nginx -v 2>&1)
if [[ "${nginx_version}" != *"1.18."* ]]; then
echo "==> installer: installing nginx 1.18"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-2~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
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
# Cloudron 6 on ubuntu 20 installed recommended packages of collectd -> libinotify -> gnome->shell
apt remove -y gnome-shell || true
apt -y autoremove || true
echo "==> installer: updating node"
if [[ "$(node --version)" != "v10.18.1" ]]; then
mkdir -p /usr/local/node-10.18.1
@@ -128,7 +124,7 @@ if ! id "${user}" 2>/dev/null; then
fi
if [[ "${is_update}" == "yes" ]]; then
echo "==> installer: stop box service for update"
echo "==> installer: stop cloudron.target service for update"
${box_src_dir}/setup/stop.sh
fi
+11 -29
View File
@@ -19,7 +19,6 @@ readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
readonly ubuntu_version=$(lsb_release -rs)
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
cp -f "${script_dir}/../scripts/cloudron-translation-update" /usr/bin/cloudron-translation-update
# this needs to match the cloudron/base:2.0.0 gid
if ! getent group media; then
@@ -32,8 +31,7 @@ systemctl enable apparmor
systemctl restart apparmor
usermod ${USER} -a -G docker
# unbound (which starts after box code) relies on this interface to exist. dockerproxy also relies on this.
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
docker network create --subnet=172.18.0.0/16 cloudron || true
mkdir -p "${BOX_DATA_DIR}"
mkdir -p "${APPS_DATA_DIR}"
@@ -46,7 +44,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/mysql"
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
mkdir -p "${PLATFORM_DATA_DIR}/redis"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail/banner"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
mkdir -p "${PLATFORM_DATA_DIR}/acme"
@@ -59,13 +57,11 @@ mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \
mkdir -p "${PLATFORM_DATA_DIR}/update"
mkdir -p "${BOX_DATA_DIR}/appicons"
mkdir -p "${BOX_DATA_DIR}/firewall"
mkdir -p "${BOX_DATA_DIR}/profileicons"
mkdir -p "${BOX_DATA_DIR}/certs"
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
mkdir -p "${BOX_DATA_DIR}/mail/dkim"
mkdir -p "${BOX_DATA_DIR}/well-known" # .well-known documents
mkdir -p "${BOX_DATA_DIR}/sftp/ssh" # sftp keys
# ensure backups folder exists and is writeable
mkdir -p /var/backups
@@ -106,11 +102,10 @@ echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
systemctl daemon-reload
systemctl enable --now cloudron-syslog
systemctl enable unbound
systemctl enable box
systemctl enable cloudron-syslog
systemctl enable cloudron.target
systemctl enable cloudron-firewall
systemctl enable --now cloudron-disable-thp
# update firewall rules
systemctl restart cloudron-firewall
@@ -132,12 +127,6 @@ echo "==> Configuring collectd"
rm -rf /etc/collectd /var/log/collectd.log
ln -sfF "${PLATFORM_DATA_DIR}/collectd" /etc/collectd
cp "${script_dir}/start/collectd/collectd.conf" "${PLATFORM_DATA_DIR}/collectd/collectd.conf"
if [[ "${ubuntu_version}" == "20.04" ]]; then
# https://bugs.launchpad.net/ubuntu/+source/collectd/+bug/1872281
if ! grep -q LD_PRELOAD /etc/default/collectd; then
echo -e "\nLD_PRELOAD=/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so" >> /etc/default/collectd
fi
fi
systemctl restart collectd
echo "==> Configuring logrotate"
@@ -195,10 +184,6 @@ fi
readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
if [[ "${ubuntu_version}" == "20.04" ]]; then
# mysql 8 added a new caching_sha2_password scheme which mysqljs does not support
mysql -u root -p${mysql_root_password} -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${mysql_root_password}';"
fi
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
# set HOME explicity, because it's not set when the installer calls it. this is done because
@@ -220,15 +205,12 @@ else
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
fi
if [[ ! -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" ]]; then
# the key format in Ubuntu 20 changed, so we create keys in legacy format. for older ubuntu, just re-use the host keys
# see https://github.com/proftpd/proftpd/issues/793
if [[ "${ubuntu_version}" == "20.04" ]]; then
ssh-keygen -m PEM -t rsa -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" -q -N ""
else
cp /etc/ssh/ssh_host_rsa_key* ${BOX_DATA_DIR}/sftp/ssh
fi
fi
# old installations used to create appdata/<app>/redis which is now part of old backups and prevents restore
echo "==> Cleaning up stale redis directories"
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} +
echo "==> Cleaning up old logs"
rm -f /home/yellowtent/platformdata/logs/*/*.log.* || true
echo "==> Changing ownership"
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
@@ -245,7 +227,7 @@ chown "${USER}:${USER}" "${BOX_DATA_DIR}/mail"
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
echo "==> Starting Cloudron"
systemctl start box
systemctl start cloudron.target
sleep 2 # give systemd sometime to start the processes
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
set -eu
echo "==> Disabling THP"
# https://docs.couchbase.com/server/current/install/thp-disable.html
if [[ -d /sys/kernel/mm/transparent_hugepage ]]; then
echo "never" > /sys/kernel/mm/transparent_hugepage/enabled
echo "never" > /sys/kernel/mm/transparent_hugepage/defrag
else
echo "==> kernel does not have THP"
fi
+10 -24
View File
@@ -6,29 +6,11 @@ echo "==> Setting up firewall"
iptables -t filter -N CLOUDRON || true
iptables -t filter -F CLOUDRON # empty any existing rules
# first setup any user IP block lists
ipset create cloudron_blocklist hash:net || true
/home/yellowtent/box/src/scripts/setblocklist.sh
iptables -t filter -A CLOUDRON -m set --match-set cloudron_blocklist src -j DROP
# the DOCKER-USER chain is not cleared on docker restart
if ! iptables -t filter -C DOCKER-USER -m set --match-set cloudron_blocklist src -j DROP; then
iptables -t filter -I DOCKER-USER 1 -m set --match-set cloudron_blocklist src -j DROP
fi
# allow related and establisted connections
iptables -t filter -A CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -t filter -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443 -j ACCEPT # 202 is the alternate ssh port
# whitelist any user ports
ports_json="/home/yellowtent/boxdata/firewall/ports.json"
if allowed_tcp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_tcp_ports.join(','))" 2>/dev/null); then
[[ -n "${allowed_tcp_ports}" ]] && iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports "${allowed_tcp_ports}" -j ACCEPT
fi
if allowed_udp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_udp_ports.join(','))" 2>/dev/null); then
[[ -n "${allowed_tcp_ports}" ]] && iptables -A CLOUDRON -p udp -m udp -m multiport --dports "${allowed_tcp_ports}" -j ACCEPT
fi
# NOTE: keep these in sync with src/apps.js validatePortBindings
# allow ssh, http, https, ping, dns
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
@@ -70,6 +52,8 @@ for port in 22 202; do
iptables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --update --name "public-${port}" --seconds 10 --hitcount 5 -j CLOUDRON_RATELIMIT_LOG
done
# TODO: move docker platform rules to platform.js so it can be specialized to rate limit only when destination is the mail container
# docker translates (dnat) 25, 587, 993, 4190 in the PREROUTING step
for port in 2525 4190 9993; do
iptables -A CLOUDRON_RATELIMIT -p tcp --syn ! -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 50 -j CLOUDRON_RATELIMIT_LOG
@@ -85,10 +69,12 @@ for port in 3306 5432 6379 27017; do
iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
done
# For ssh, http, https
if ! iptables -t filter -C INPUT -j CLOUDRON_RATELIMIT 2>/dev/null; then
iptables -t filter -I INPUT 1 -j CLOUDRON_RATELIMIT
fi
# Workaround issue where Docker insists on adding itself first in FORWARD table
# For smtp, imap etc routed via docker/nat
# Workaroud issue where Docker insists on adding itself first in FORWARD table
iptables -D FORWARD -j CLOUDRON_RATELIMIT || true
iptables -I FORWARD 1 -j CLOUDRON_RATELIMIT
+4 -11
View File
@@ -10,19 +10,12 @@ if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
fi
echo "${ip}" > /tmp/.cloudron-motd-cache
if [[ ! -f /etc/cloudron/SETUP_TOKEN ]]; then
url="https://${ip}"
else
setupToken="$(cat /etc/cloudron/SETUP_TOKEN)"
url="https://${ip}/?setupToken=${setupToken}"
fi
printf "\t\t\tWELCOME TO CLOUDRON\n"
printf "\t\t\t-------------------\n"
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit ${url} on your browser and accept the self-signed certificate to finish setup."
printf "Cloudron overview - https://docs.cloudron.io/ \n"
printf "Cloudron setup - https://docs.cloudron.io/installation/#setup \n"
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://${ip} on your browser and accept the self-signed certificate to finish setup."
printf "Cloudron overview - https://cloudron.io/documentation/ \n"
printf "Cloudron setup - https://cloudron.io/documentation/installation/#setup \n"
else
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
printf "\t\t\t-----------------------\n"
@@ -30,7 +23,7 @@ else
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
printf "are automatically installed on this server every night.\n"
printf "\n"
printf "Read more at https://docs.cloudron.io/security/#os-updates\n"
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
fi
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
-5
View File
@@ -4,11 +4,6 @@ set -eu -o pipefail
readonly APPS_SWAP_FILE="/apps.swap"
if [[ -f "${APPS_SWAP_FILE}" ]]; then
echo "Swap file already exists at /apps.swap . Skipping"
exit
fi
# all sizes are in mb
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly swap_size=$((${physical_memory} > 4096 ? 4096 : ${physical_memory})) # min(RAM, 4GB) if you change this, fix enoughResourcesAvailable() in client.js
+33 -2
View File
@@ -121,7 +121,7 @@ LoadPlugin memory
#LoadPlugin netlink
#LoadPlugin network
#LoadPlugin nfs
#LoadPlugin nginx
LoadPlugin nginx
#LoadPlugin notify_desktop
#LoadPlugin notify_email
#LoadPlugin ntpd
@@ -149,7 +149,7 @@ LoadPlugin memory
#LoadPlugin statsd
LoadPlugin swap
#LoadPlugin table
#LoadPlugin tail
LoadPlugin tail
#LoadPlugin tail_csv
#LoadPlugin tcpconns
#LoadPlugin teamspeak2
@@ -197,11 +197,42 @@ LoadPlugin write_graphite
IgnoreSelected false
</Plugin>
<Plugin nginx>
URL "http://127.0.0.1/nginx_status"
</Plugin>
<Plugin swap>
ReportByDevice false
ReportBytes true
</Plugin>
<Plugin "tail">
<File "/var/log/nginx/error.log">
Instance "nginx"
<Match>
Regex ".*"
DSType "CounterInc"
Type counter
Instance "errors"
</Match>
</File>
<File "/var/log/nginx/access.log">
Instance "nginx"
<Match>
Regex ".*"
DSType "CounterInc"
Type counter
Instance "requests"
</Match>
<Match>
Regex " \".*\" [0-9]+ [0-9]+ ([0-9]+)"
DSType GaugeAverage
Type delay
Instance "response"
</Match>
</File>
</Plugin>
<Plugin python>
# https://blog.dbrgn.ch/2017/3/10/write-a-collectd-python-plugin/
ModulePath "/home/yellowtent/box/setup/start/collectd/"
+1 -1
View File
@@ -6,7 +6,7 @@ disks = []
def init():
global disks
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).decode('utf-8').splitlines()]
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).splitlines()]
disks = lines[1:] # strip header
collectd.info('custom df plugin initialized with %s' % disks)
+1 -1
View File
@@ -6,7 +6,7 @@
performance_schema=OFF
max_connections=50
# on ec2, without this we get a sporadic connection drop when doing the initial migration
max_allowed_packet=64M
max_allowed_packet=32M
# https://mathiasbynens.be/notes/mysql-utf8mb4
character-set-server = utf8mb4
+19 -1
View File
@@ -6,7 +6,7 @@ worker_processes auto;
# this is 4096 by default. See /proc/<PID>/limits and /etc/security/limits.conf
# usually twice the worker_connections (one for uptsream, one for downstream)
# see also LimitNOFILE=16384 in systemd drop-in
worker_rlimit_nofile 8192;
worker_rlimit_nofile 8192;
pid /run/nginx.pid;
@@ -43,5 +43,23 @@ http {
# zones for rate limiting
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/s; # 10 request a second
# default http server that returns 404 for any domain we are not listening on
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name does_not_match_anything;
# acme challenges (for app installation and re-configure when the vhost config does not exist)
location /.well-known/acme-challenge/ {
default_type text/plain;
alias /home/yellowtent/platformdata/acme/;
}
location / {
return 404;
}
}
include applications/*.conf;
}
-12
View File
@@ -50,15 +50,3 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.s
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
Defaults!/home/yellowtent/box/src/scripts/rmmailbox.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmmailbox.sh
Defaults!/home/yellowtent/box/src/scripts/starttask.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/starttask.sh
Defaults!/home/yellowtent/box/src/scripts/stoptask.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/stoptask.sh
Defaults!/home/yellowtent/box/src/scripts/setblocklist.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/setblocklist.sh
+4 -5
View File
@@ -1,21 +1,20 @@
[Unit]
Description=Cloudron Admin
OnFailure=crashnotifier@%n.service
StopWhenUnneeded=true
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
BindsTo=systemd-journald.service
After=mysql.service nginx.service
; As cloudron-resize-fs is a one-shot, the Wants= automatically ensures that the service *finishes*
Wants=cloudron-resize-fs.service
[Install]
WantedBy=multi-user.target
[Service]
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart=/home/yellowtent/box/box.js
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box:*,connect-lastmile,-box:ldap" "BOX_ENV=cloudron" "NODE_ENV=production"
; Systemd does not append logs when logging to files, we spawn a shell first and exec to replace it after setting up the pipes
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well
KillMode=control-group
; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
@@ -1,15 +0,0 @@
# https://docs.mongodb.com/manual/tutorial/transparent-huge-pages/
[Unit]
Description=Disable Transparent Huge Pages (THP)
DefaultDependencies=no
After=sysinit.target local-fs.target
Before=docker.service
[Service]
Type=oneshot
ExecStart="/home/yellowtent/box/setup/start/cloudron-disable-thp.sh"
RemainAfterExit=yes
[Install]
WantedBy=basic.target
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Cloudron Smartserver
Documentation=https://cloudron.io/documentation.html
StopWhenUnneeded=true
Requires=box.service
After=box.service
# AllowIsolate=yes
[Install]
WantedBy=multi-user.target
+1 -1
View File
@@ -2,7 +2,7 @@
[Unit]
Description=Unbound DNS Resolver
After=network.target docker.service
After=network.target
[Service]
PIDFile=/run/unbound.pid
+1 -3
View File
@@ -1,7 +1,5 @@
server:
port: 53
interface: 127.0.0.1
interface: 172.18.0.1
interface: 0.0.0.0
do-ip6: no
access-control: 127.0.0.1 allow
access-control: 172.18.0.1/16 allow
+1 -1
View File
@@ -4,4 +4,4 @@ set -eu -o pipefail
echo "Stopping cloudron"
systemctl stop box
systemctl stop cloudron.target
File diff suppressed because it is too large Load Diff
+125 -103
View File
@@ -1,32 +1,32 @@
'use strict';
exports = module.exports = {
get,
add,
exists,
del,
update,
getAll,
getPortBindings,
delPortBinding,
get: get,
getByHttpPort: getByHttpPort,
getByContainerId: getByContainerId,
add: add,
exists: exists,
del: del,
update: update,
getAll: getAll,
getPortBindings: getPortBindings,
delPortBinding: delPortBinding,
setAddonConfig,
getAddonConfig,
getAddonConfigByAppId,
getAddonConfigByName,
unsetAddonConfig,
unsetAddonConfigByAppId,
getAppIdByAddonConfigValue,
getByIpAddress,
setAddonConfig: setAddonConfig,
getAddonConfig: getAddonConfig,
getAddonConfigByAppId: getAddonConfigByAppId,
getAddonConfigByName: getAddonConfigByName,
unsetAddonConfig: unsetAddonConfig,
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
setHealth,
setTask,
getAppStoreIds,
setHealth: setHealth,
setTask: setTask,
getAppStoreIds: getAppStoreIds,
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
SUBDOMAIN_TYPE_ALIAS: 'alias',
_clear: clear
};
@@ -38,14 +38,17 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson',
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp',
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.bindsJson',
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
const SUBDOMAIN_FIELDS = [ 'appId', 'domain', 'subdomain', 'type' ].join(',');
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
@@ -86,7 +89,6 @@ function postProcess(result) {
result.sso = !!result.sso; // make it bool
result.enableBackup = !!result.enableBackup; // make it bool
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
result.proxyAuth = !!result.proxyAuth;
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
@@ -96,23 +98,15 @@ function postProcess(result) {
result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {};
delete result.servicesConfigJson;
let subdomains = JSON.parse(result.subdomains), domains = JSON.parse(result.domains), subdomainTypes = JSON.parse(result.subdomainTypes);
delete result.subdomains;
delete result.domains;
delete result.subdomainTypes;
assert(result.bindsJson === null || typeof result.bindsJson === 'string');
result.binds = safe.JSON.parse(result.bindsJson) || {};
delete result.bindsJson;
result.alternateDomains = [];
result.aliasDomains = [];
for (let i = 0; i < subdomainTypes.length; i++) {
if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_PRIMARY) {
result.location = subdomains[i];
result.domain = domains[i];
} else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_REDIRECT) {
result.alternateDomains.push({ domain: domains[i], subdomain: subdomains[i] });
} else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_ALIAS) {
result.aliasDomains.push({ domain: domains[i], subdomain: subdomains[i] });
}
}
result.alternateDomains = result.alternateDomains || [];
result.alternateDomains.forEach(function (d) {
delete d.appId;
delete d.type;
});
let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues);
delete result.envNames;
@@ -122,67 +116,119 @@ function postProcess(result) {
if (envNames[i]) result.env[envNames[i]] = envValues[i];
}
let volumeIds = JSON.parse(result.volumeIds);
delete result.volumeIds;
let volumeReadOnlys = JSON.parse(result.volumeReadOnlys);
delete result.volumeReadOnlys;
result.mounts = volumeIds[0] === null ? [] : volumeIds.map((v, idx) => { return { volumeId: v, readOnly: !!volumeReadOnlys[idx] }; }); // NOTE: volumeIds is [null] when volumes of an app is empty
result.error = safe.JSON.parse(result.errorJson);
delete result.errorJson;
result.taskId = result.taskId ? String(result.taskId) : null;
}
// each query simply join apps table with another table by id. we then join the full result together
const PB_QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id';
const ENV_QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id';
const SUBDOMAIN_QUERY = 'SELECT id, JSON_ARRAYAGG(subdomains.subdomain) AS subdomains, JSON_ARRAYAGG(subdomains.domain) AS domains, JSON_ARRAYAGG(subdomains.type) AS subdomainTypes FROM apps LEFT JOIN subdomains ON apps.id = subdomains.appId GROUP BY apps.id';
const MOUNTS_QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id';
const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, subdomains, domains, subdomainTypes, volumeIds, volumeReadOnlys FROM apps`
+ ` LEFT JOIN (${PB_QUERY}) AS q1 on q1.id = apps.id`
+ ` LEFT JOIN (${ENV_QUERY}) AS q2 on q2.id = apps.id`
+ ` LEFT JOIN (${SUBDOMAIN_QUERY}) AS q3 on q3.id = apps.id`
+ ` LEFT JOIN (${MOUNTS_QUERY}) AS q4 on q4.id = apps.id`;
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query(`${APPS_QUERY} WHERE apps.id = ?`, [ id ], function (error, result) {
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, '
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
postProcess(result[0]);
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
});
}
function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
function getByHttpPort(httpPort, callback) {
assert.strictEqual(typeof httpPort, 'number');
assert.strictEqual(typeof callback, 'function');
database.query(`${APPS_QUERY} WHERE apps.containerIp = ?`, [ ip ], function (error, result) {
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
postProcess(result[0]);
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
});
}
function getByContainerId(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query(`${APPS_QUERY} ORDER BY apps.id`, [ ], function (error, results) {
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(postProcess);
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
alternateDomains.forEach(function (d) {
var domain = results.find(function (a) { return d.appId === a.id; });
if (!domain) return;
domain.alternateDomains = domain.alternateDomains || [];
domain.alternateDomains.push(d);
});
results.forEach(postProcess);
callback(null, results);
});
});
}
@@ -254,15 +300,6 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
});
}
if (data.aliasDomains) {
data.aliasDomains.forEach(function (d) {
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_ALIAS ]
});
});
}
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'no such domain'));
@@ -321,13 +358,12 @@ function del(id, callback) {
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appPasswords WHERE identifier = ?', args: [ id ] },
{ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
];
database.transaction(queries, function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results[5].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
if (results[4].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
callback(null);
});
@@ -360,7 +396,6 @@ function updateWithConstraints(id, app, constraints, callback) {
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
assert(!('aliasDomains' in app) || Array.isArray(app.aliasDomains));
assert(!('tags' in app) || Array.isArray(app.tags));
assert(!('env' in app) || typeof app.env === 'object');
@@ -388,35 +423,22 @@ function updateWithConstraints(id, app, constraints, callback) {
}
if ('location' in app && 'domain' in app) { // must be updated together as they are unique together
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ]}); // all locations of an app must be updated together
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.location, exports.SUBDOMAIN_TYPE_PRIMARY ]});
if ('alternateDomains' in app) {
app.alternateDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
});
}
if ('aliasDomains' in app) {
app.aliasDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_ALIAS ]});
});
}
queries.push({ query: 'UPDATE subdomains SET subdomain = ?, domain = ? WHERE appId = ? AND type = ?', args: [ app.location, app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('mounts' in app) {
queries.push({ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ]});
app.mounts.forEach(function (m) {
queries.push({ query: 'INSERT INTO appMounts (appId, volumeId, readOnly) VALUES (?, ?, ?)', args: [ id, m.volumeId, m.readOnly ]});
if ('alternateDomains' in app) {
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ? AND type = ?', args: [ id, exports.SUBDOMAIN_TYPE_REDIRECT ]});
app.alternateDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
});
}
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig') {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'binds') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'aliasDomains' && p !== 'env' && p !== 'mounts') {
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
fields.push(p + ' = ?');
values.push(app[p]);
}
+15 -9
View File
@@ -14,14 +14,13 @@ var appdb = require('./appdb.js'),
util = require('util');
exports = module.exports = {
run
run: run
};
const HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
const UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
const OOM_EVENT_LIMIT = 60 * 60 * 1000; // 60 minutes
const gStartTime = new Date(); // time when apphealthmonitor was started
let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5 minutes ago
function debugApp(app) {
@@ -35,8 +34,7 @@ function setHealth(app, health, callback) {
assert.strictEqual(typeof health, 'string');
assert.strictEqual(typeof callback, 'function');
let now = new Date(), curHealth = app.health;
let healthTime = gStartTime > app.healthTime ? gStartTime : app.healthTime; // on box restart, clamp value to start time
let now = new Date(), healthTime = app.healthTime, curHealth = app.health;
if (health === apps.HEALTH_HEALTHY) {
healthTime = now;
@@ -81,13 +79,21 @@ function checkAppHealth(app, callback) {
const manifest = app.manifest;
docker.inspect(app.containerId, function (error, data) {
if (error || !data || !data.State) return setHealth(app, apps.HEALTH_ERROR, callback);
if (data.State.Running !== true) return setHealth(app, apps.HEALTH_DEAD, callback);
if (error || !data || !data.State) {
debugApp(app, 'Error inspecting container');
return setHealth(app, apps.HEALTH_ERROR, callback);
}
if (data.State.Running !== true) {
debugApp(app, 'exited');
return setHealth(app, apps.HEALTH_DEAD, callback);
}
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
const healthCheckUrl = `http://${app.containerIp}:${manifest.httpPort}${manifest.healthCheckPath}`;
// poll through docker network instead of nginx to bypass any potential oauth proxy
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
superagent
.get(healthCheckUrl)
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
@@ -97,7 +103,7 @@ function checkAppHealth(app, callback) {
.end(function (error, res) {
if (error && !error.response) {
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 403) { // 2xx and 3xx are ok. even 401 and 403 are ok for now (for WP sites)
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, apps.HEALTH_HEALTHY, callback);
@@ -120,7 +126,7 @@ function getContainerInfo(containerId, callback) {
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:2.0.0 /bin/bash
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
+151 -196
View File
@@ -1,72 +1,70 @@
'use strict';
exports = module.exports = {
hasAccessTo,
removeInternalFields,
removeRestrictedFields,
hasAccessTo: hasAccessTo,
removeInternalFields: removeInternalFields,
removeRestrictedFields: removeRestrictedFields,
get,
getByIpAddress,
getByFqdn,
getAll,
getAllByUser,
install,
uninstall,
get: get,
getByContainerId: getByContainerId,
getByIpAddress: getByIpAddress,
getByFqdn: getByFqdn,
getAll: getAll,
getAllByUser: getAllByUser,
install: install,
uninstall: uninstall,
setAccessRestriction,
setLabel,
setIcon,
setTags,
setMemoryLimit,
setCpuShares,
setMounts,
setAutomaticBackup,
setAutomaticUpdate,
setReverseProxyConfig,
setCertificate,
setDebugMode,
setEnvironment,
setMailbox,
setLocation,
setDataDir,
repair,
setAccessRestriction: setAccessRestriction,
setLabel: setLabel,
setIcon: setIcon,
setTags: setTags,
setMemoryLimit: setMemoryLimit,
setCpuShares: setCpuShares,
setBinds: setBinds,
setAutomaticBackup: setAutomaticBackup,
setAutomaticUpdate: setAutomaticUpdate,
setReverseProxyConfig: setReverseProxyConfig,
setCertificate: setCertificate,
setDebugMode: setDebugMode,
setEnvironment: setEnvironment,
setMailbox: setMailbox,
setLocation: setLocation,
setDataDir: setDataDir,
repair: repair,
restore,
importApp,
exportApp,
clone,
restore: restore,
importApp: importApp,
clone: clone,
update,
update: update,
backup,
listBackups,
backup: backup,
listBackups: listBackups,
getLocalLogfilePaths,
getLogs,
getLogs: getLogs,
start,
stop,
restart,
start: start,
stop: stop,
restart: restart,
exec,
exec: exec,
checkManifestConstraints,
downloadManifest,
checkManifestConstraints: checkManifestConstraints,
downloadManifest: downloadManifest,
canAutoupdateApp,
autoupdateApps,
canAutoupdateApp: canAutoupdateApp,
autoupdateApps: autoupdateApps,
restoreInstalledApps,
configureInstalledApps,
schedulePendingTasks,
restartAppsUsingAddons,
restoreInstalledApps: restoreInstalledApps,
configureInstalledApps: configureInstalledApps,
schedulePendingTasks: schedulePendingTasks,
restartAppsUsingAddons: restartAppsUsingAddons,
getDataDir,
getIconPath,
getMemoryLimit,
getDataDir: getDataDir,
getIconPath: getIconPath,
downloadFile,
uploadFile,
downloadFile: downloadFile,
uploadFile: uploadFile,
PORT_TYPE_TCP: 'tcp',
PORT_TYPE_UDP: 'udp',
@@ -136,6 +134,7 @@ var appdb = require('./appdb.js'),
superagent = require('superagent'),
tasks = require('./tasks.js'),
TransformStream = require('stream').Transform,
updateChecker = require('./updatechecker.js'),
users = require('./users.js'),
util = require('util'),
uuid = require('uuid'),
@@ -155,6 +154,7 @@ function validatePortBindings(portBindings, manifest) {
const RESERVED_PORTS = [
22, /* ssh */
25, /* smtp */
53, /* dns */
80, /* http */
143, /* imap */
202, /* alternate ssh */
@@ -167,7 +167,7 @@ function validatePortBindings(portBindings, manifest) {
2004, /* graphite (lo) */
2514, /* cloudron-syslog (lo) */
constants.PORT, /* app server (lo) */
constants.AUTHWALL_PORT, /* protected sites */
constants.SYSADMIN_PORT, /* sysadmin app server (lo) */
constants.INTERNAL_SMTP_PORT, /* internal smtp port (lo) */
constants.LDAP_PORT,
3306, /* mysql (lo) */
@@ -191,7 +191,7 @@ function validatePortBindings(portBindings, manifest) {
if (!Number.isInteger(hostPort)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not an integer`, { field: 'portBindings', portName: portName });
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} is reserved.`, { field: 'portBindings', portName: portName });
if (RESERVED_PORT_RANGES.find(range => (hostPort >= range[0] && hostPort <= range[1]))) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} is reserved.`, { field: 'portBindings', portName: portName });
if (hostPort !== 53 && (hostPort <= 1023 || hostPort > 65535)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not in permitted range`, { field: 'portBindings', portName: portName }); // dns 53 is special and adblocker apps can use them
if (hostPort <= 1023 || hostPort > 65535) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not in permitted range`, { field: 'portBindings', portName: portName });
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
@@ -334,6 +334,20 @@ function validateEnv(env) {
return null;
}
function validateBinds(binds) {
for (let name of Object.keys(binds)) {
// just have friendly characters under /media
if (!/^[-0-9a-zA-Z_@$=#.%+]+$/.test(name)) return new BoxError(BoxError.BAD_FIELD, `Invalid bind name: ${name}`);
const bind = binds[name];
if (!bind.hostPath.startsWith('/mnt') && !bind.hostPath.startsWith('/media')) return new BoxError(BoxError.BAD_FIELD, 'hostPath must be in /mnt or /media');
if (path.normalize(bind.hostPath) !== bind.hostPath) return new BoxError(BoxError.BAD_FIELD, 'hostPath is not normalized');
}
return null;
}
function validateDataDir(dataDir) {
if (dataDir === null) return null;
@@ -384,7 +398,7 @@ function getDuplicateErrorDetails(errorMessage, locations, domainObjectMap, port
// check if any of the port bindings conflict
for (let portName in portBindings) {
if (portBindings[portName] === parseInt(match[1])) return new BoxError(BoxError.ALREADY_EXISTS, `Port ${match[1]} is in use`, { portName });
if (portBindings[portName] === parseInt(match[1])) return new BoxError(BoxError.ALREADY_EXISTS, `Port ${match[1]} is reserved`, { portName });
}
if (match[2] === 'dataDir') {
@@ -406,13 +420,13 @@ function removeInternalFields(app) {
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares',
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
'label', 'alternateDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts');
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'binds');
}
// non-admins can only see these
function removeRestrictedFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'alternateDomains', 'aliasDomains', 'sso',
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'alternateDomains', 'sso',
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup');
}
@@ -446,20 +460,6 @@ function getIconPath(app, options, callback) {
callback(new BoxError(BoxError.NOT_FOUND, 'No icon'));
}
function getMemoryLimit(app) {
assert.strictEqual(typeof app, 'object');
let memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
if (memoryLimit === -1) { // unrestricted
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit < constants.DEFAULT_MEMORY_LIMIT) { // ensure we never go below minimum (in case we change the default)
memoryLimit = constants.DEFAULT_MEMORY_LIMIT;
}
return memoryLimit;
}
function postProcess(app, domainObjectMap) {
let result = {};
for (let portName in app.portBindings) {
@@ -470,7 +470,6 @@ function postProcess(app, domainObjectMap) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
app.aliasDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
}
function hasAccessTo(app, user, callback) {
@@ -513,6 +512,25 @@ function get(appId, callback) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
if (error) return callback(error);
postProcess(app, domainObjectMap);
callback(null, app);
});
});
}
function getByContainerId(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
getDomainObjectMap(function (error, domainObjectMap) {
if (error) return callback(error);
appdb.getByContainerId(containerId, function (error, app) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
if (error) return callback(error);
postProcess(app, domainObjectMap);
@@ -530,15 +548,16 @@ function getByIpAddress(ip, callback) {
// this is only used by the ldap test. the apps tests still uses proper docker
if (constants.TEST && exports._MOCK_GET_BY_IP_APP_ID) return get(exports._MOCK_GET_BY_IP_APP_ID, callback);
appdb.getByIpAddress(ip, function (error, app) {
docker.getContainerIdByIp(ip, function (error, containerId) {
if (error) return callback(error);
getDomainObjectMap(function (error, domainObjectMap) {
docker.inspect(containerId, function (error, result) {
if (error) return callback(error);
postProcess(app, domainObjectMap);
const appId = safe.query(result, 'Config.Labels.appId', null);
if (!appId) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
callback(null, app);
get(appId, callback);
});
});
}
@@ -620,31 +639,20 @@ function scheduleTask(appId, installationState, taskId, callback) {
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
let memoryLimit = 400;
if (installationState === exports.ISTATE_PENDING_BACKUP || installationState === exports.ISTATE_PENDING_CLONE || installationState === exports.ISTATE_PENDING_RESTORE) {
memoryLimit = 'memoryLimit' in backupConfig ? Math.max(backupConfig.memoryLimit/1024/1024, 400) : 400;
appTaskManager.scheduleTask(appId, taskId, function (error) {
debug(`scheduleTask: task ${taskId} of ${appId} completed`);
if (error && (error.code === tasks.ECRASHED || error.code === tasks.ESTOPPED)) { // if task crashed, update the error
debug(`Apptask crashed/stopped: ${error.message}`);
let boxError = new BoxError(BoxError.TASK_ERROR, error.message);
boxError.details.crashed = error.code === tasks.ECRASHED;
boxError.details.stopped = error.code === tasks.ESTOPPED;
// see also apptask makeTaskError
boxError.details.taskId = taskId;
boxError.details.installationState = installationState;
appdb.update(appId, { installationState: exports.ISTATE_ERROR, error: boxError.toPlainObject(), taskId: null }, callback);
} else if (!(installationState === exports.ISTATE_PENDING_UNINSTALL && !error)) { // clear out taskId except for successful uninstall
appdb.update(appId, { taskId: null }, callback);
}
const options = { timeout: 20 * 60 * 60 * 1000 /* 20 hours */, nice: 15, memoryLimit };
appTaskManager.scheduleTask(appId, taskId, options, function (error) {
debug(`scheduleTask: task ${taskId} of ${appId} completed`);
if (error && (error.code === tasks.ECRASHED || error.code === tasks.ESTOPPED)) { // if task crashed, update the error
debug(`Apptask crashed/stopped: ${error.message}`);
let boxError = new BoxError(BoxError.TASK_ERROR, error.message);
boxError.details.crashed = error.code === tasks.ECRASHED;
boxError.details.stopped = error.code === tasks.ESTOPPED;
// see also apptask makeTaskError
boxError.details.taskId = taskId;
boxError.details.installationState = installationState;
appdb.update(appId, { installationState: exports.ISTATE_ERROR, error: boxError.toPlainObject(), taskId: null }, callback);
} else if (!(installationState === exports.ISTATE_PENDING_UNINSTALL && !error)) { // clear out taskId except for successful uninstall
appdb.update(appId, { taskId: null }, callback);
}
});
});
}
@@ -706,13 +714,7 @@ function validateLocations(locations, callback) {
for (let location of locations) {
if (!(location.domain in domainObjectMap)) return callback(new BoxError(BoxError.BAD_FIELD, 'No such domain', { field: 'location', domain: location.domain, subdomain: location.subdomain }));
let subdomain = location.subdomain;
if (location.type === 'alias' && subdomain.startsWith('*')) {
if (subdomain === '*') continue;
subdomain = subdomain.replace(/^\*\./, ''); // remove *.
}
error = domains.validateHostname(subdomain, domainObjectMap[location.domain]);
error = domains.validateHostname(location.subdomain, domainObjectMap[location.domain]);
if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Bad location: ' + error.message, { field: 'location', domain: location.domain, subdomain: location.subdomain }));
}
@@ -744,7 +746,6 @@ function install(data, auditSource, callback) {
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true,
alternateDomains = data.alternateDomains || [],
aliasDomains = data.aliasDomains || [],
env = data.env || {},
label = data.label || null,
tags = data.tags || [],
@@ -778,7 +779,7 @@ function install(data, auditSource, callback) {
if ('sso' in data && !('optionalSso' in manifest)) return callback(new BoxError(BoxError.BAD_FIELD, 'sso can only be specified for apps with optionalSso'));
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['proxyAuth'];
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
error = validateEnv(env);
if (error) return callback(error);
@@ -797,10 +798,7 @@ function install(data, auditSource, callback) {
}
}
const locations = [{ subdomain: location, domain, type: 'primary' }]
.concat(alternateDomains.map(ad => _.extend(ad, { type: 'redirect' })))
.concat(aliasDomains.map(ad => _.extend(ad, { type: 'alias' })));
const locations = [{subdomain: location, domain}].concat(alternateDomains);
validateLocations(locations, function (error, domainObjectMap) {
if (error) return callback(error);
@@ -812,19 +810,18 @@ function install(data, auditSource, callback) {
debug('Will install app with id : ' + appId);
var data = {
accessRestriction,
memoryLimit,
sso,
debugMode,
mailboxName,
mailboxDomain,
enableBackup,
enableAutomaticUpdate,
alternateDomains,
aliasDomains,
env,
label,
tags,
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
sso: sso,
debugMode: debugMode,
mailboxName: mailboxName,
mailboxDomain: mailboxDomain,
enableBackup: enableBackup,
enableAutomaticUpdate: enableAutomaticUpdate,
alternateDomains: alternateDomains,
env: env,
label: label,
tags: tags,
runState: exports.RSTATE_RUNNING,
installationState: exports.ISTATE_PENDING_INSTALL
};
@@ -854,7 +851,6 @@ function install(data, auditSource, callback) {
const newApp = _.extend({}, data, { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]);
newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
newApp.aliasDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId, app: newApp, taskId: result.taskId });
@@ -997,9 +993,9 @@ function setCpuShares(app, cpuShares, auditSource, callback) {
});
}
function setMounts(app, mounts, auditSource, callback) {
function setBinds(app, binds, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert(Array.isArray(mounts));
assert(binds && typeof binds === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1007,15 +1003,17 @@ function setMounts(app, mounts, auditSource, callback) {
let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
if (error) return callback(error);
error = validateBinds(binds);
if (error) return callback(error);
const task = {
args: {},
values: { mounts }
values: { binds }
};
addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(new BoxError(BoxError.CONFLICT, 'Duplicate mount points'));
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mounts, taskId: result.taskId });
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, binds, taskId: result.taskId });
callback(null, { taskId: result.taskId });
});
@@ -1210,8 +1208,7 @@ function setLocation(app, data, auditSource, callback) {
domain: data.domain.toLowerCase(),
// these are intentionally reset, if not set
portBindings: null,
alternateDomains: [],
aliasDomains: []
alternateDomains: []
};
if ('portBindings' in data) {
@@ -1231,20 +1228,14 @@ function setLocation(app, data, auditSource, callback) {
values.alternateDomains = data.alternateDomains;
}
if ('aliasDomains' in data) {
values.aliasDomains = data.aliasDomains;
}
const locations = [{ subdomain: values.location, domain: values.domain, type: 'primary' }]
.concat(values.alternateDomains.map(ad => _.extend(ad, { type: 'redirect' })))
.concat(values.aliasDomains.map(ad => _.extend(ad, { type: 'alias' })));
const locations = [{subdomain: values.location, domain: values.domain}].concat(values.alternateDomains);
validateLocations(locations, function (error, domainObjectMap) {
if (error) return callback(error);
const task = {
args: {
oldConfig: _.pick(app, 'location', 'domain', 'fqdn', 'alternateDomains', 'aliasDomains', 'portBindings'),
oldConfig: _.pick(app, 'location', 'domain', 'fqdn', 'alternateDomains', 'portBindings'),
overwriteDns: !!data.overwriteDns
},
values
@@ -1255,7 +1246,6 @@ function setLocation(app, data, auditSource, callback) {
values.fqdn = domains.fqdn(values.location, domainObjectMap[values.domain]);
values.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
values.aliasDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, _.extend({ appId, app, taskId: result.taskId }, values));
@@ -1299,8 +1289,7 @@ function update(app, data, auditSource, callback) {
const skipBackup = !!data.skipBackup,
appId = app.id,
manifest = data.manifest,
appStoreId = data.appStoreId;
manifest = data.manifest;
let values = {};
@@ -1315,12 +1304,14 @@ function update(app, data, auditSource, callback) {
error = checkManifestConstraints(manifest);
if (error) return callback(error);
var updateConfig = { skipBackup, manifest, appStoreId }; // this will clear appStoreId when updating from a repo and set it if passed in for update route
var updateConfig = { skipBackup, manifest };
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
if (app.manifest.id !== updateConfig.manifest.id) {
if (!data.force) return callback(new BoxError(BoxError.BAD_FIELD, 'manifest id does not match. force to override'));
// clear appStoreId so that this app does not get updates anymore
updateConfig.appStoreId = '';
}
// suffix '0' if prerelease is missing for semver.lte to work as expected
@@ -1367,23 +1358,13 @@ function update(app, data, auditSource, callback) {
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId, app, skipBackup, toManifest: manifest, fromManifest: app.manifest, force: data.force, taskId: result.taskId });
// clear update indicator, if update fails, it will come back through the update checker
updateChecker.resetAppUpdateInfo(appId);
callback(null, { taskId: result.taskId });
});
}
function getLocalLogfilePaths(app) {
assert.strictEqual(typeof app, 'object');
const appId = app.id;
var filePaths = [];
filePaths.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
filePaths.push(path.join(paths.LOG_DIR, appId, 'app.log'));
if (app.manifest.addons && app.manifest.addons.redis) filePaths.push(path.join(paths.LOG_DIR, `redis-${appId}/app.log`));
return filePaths;
}
function getLogs(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert(options && typeof options === 'object');
@@ -1403,8 +1384,11 @@ function getLogs(app, options, callback) {
var args = [ '--lines=' + lines ];
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
args.push(path.join(paths.LOG_DIR, appId, 'app.log'));
if (app.manifest.addons && app.manifest.addons.redis) args.push(path.join(paths.LOG_DIR, `redis-${appId}/app.log`));
var cp = spawn('/usr/bin/tail', args.concat(getLocalLogfilePaths(app)));
var cp = spawn('/usr/bin/tail', args);
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
@@ -1592,26 +1576,6 @@ function importApp(app, data, auditSource, callback) {
});
}
function exportApp(app, data, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_BACKUP);
if (error) return callback(error);
const task = {
args: { snapshotOnly: true },
values: {}
};
addTask(appId, exports.ISTATE_PENDING_BACKUP, task, (error, result) => {
if (error) return callback(error);
callback(null, { taskId: result.taskId });
});
}
function purchaseApp(data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1666,7 +1630,7 @@ function clone(app, data, user, auditSource, callback) {
let mailboxName = hasMailAddon(manifest) ? mailboxNameForLocation(location, manifest) : null;
let mailboxDomain = hasMailAddon(manifest) ? domain : null;
const locations = [{ subdomain: location, domain, type: 'primary' }];
const locations = [{subdomain: location, domain}];
validateLocations(locations, function (error, domainObjectMap) {
if (error) return callback(error);
@@ -1683,8 +1647,7 @@ function clone(app, data, user, auditSource, callback) {
enableBackup: app.enableBackup,
reverseProxyConfig: app.reverseProxyConfig,
env: app.env,
alternateDomains: [],
aliasDomains: []
alternateDomains: []
};
appdb.add(newAppId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) {
@@ -1706,8 +1669,6 @@ function clone(app, data, user, auditSource, callback) {
const newApp = _.extend({}, data, { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]);
newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
newApp.aliasDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: newApp, taskId: result.taskId });
callback(null, { id: newAppId, taskId: result.taskId });
@@ -2061,17 +2022,11 @@ function restartAppsUsingAddons(changedAddons, callback) {
args: {},
values: { runState: exports.RSTATE_RUNNING }
};
addTask(app.id, exports.ISTATE_PENDING_RESTART, task, function (error, result) {
if (error) debug(`restartAppsUsingAddons: error marking ${app.fqdn} for restart: ${JSON.stringify(error)}`);
else debug(`restartAppsUsingAddons: marked ${app.id} for restart with taskId ${result.taskId}`);
// stop apps before updating the databases because postgres will "lock" them preventing import
docker.stopContainers(app.id, function (error) {
if (error) debug(`restartAppsUsingAddons: error stopping ${app.fqdn}`, error);
addTask(app.id, exports.ISTATE_PENDING_RESTART, task, function (error, result) {
if (error) debug(`restartAppsUsingAddons: error marking ${app.fqdn} for restart: ${JSON.stringify(error)}`);
else debug(`restartAppsUsingAddons: marked ${app.id} for restart with taskId ${result.taskId}`);
iteratorDone(); // ignore error
});
iteratorDone(); // ignore error
});
}, callback);
});
+150 -73
View File
@@ -1,28 +1,30 @@
'use strict';
exports = module.exports = {
getFeatures,
getFeatures: getFeatures,
getApps,
getApp,
getAppVersion,
getApps: getApps,
getApp: getApp,
getAppVersion: getAppVersion,
trackBeginSetup,
trackFinishedSetup,
trackBeginSetup: trackBeginSetup,
trackFinishedSetup: trackFinishedSetup,
registerWithLoginCredentials,
registerWithLoginCredentials: registerWithLoginCredentials,
purchaseApp,
unpurchaseApp,
purchaseApp: purchaseApp,
unpurchaseApp: unpurchaseApp,
getUserToken,
getSubscription,
isFreePlan,
getUserToken: getUserToken,
getSubscription: getSubscription,
isFreePlan: isFreePlan,
getAppUpdate,
getBoxUpdate,
sendAliveStatus: sendAliveStatus,
createTicket
getAppUpdate: getAppUpdate,
getBoxUpdate: getBoxUpdate,
createTicket: createTicket
};
var apps = require('./apps.js'),
@@ -31,30 +33,30 @@ var apps = require('./apps.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
path = require('path'),
groups = require('./groups.js'),
mail = require('./mail.js'),
os = require('os'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
support = require('./support.js'),
users = require('./users.js'),
util = require('util');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// These are the default options and will be adjusted once a subscription state is obtained
// Keep in sync with appstore/routes/cloudrons.js
let gFeatures = {
userMaxCount: 5,
userGroups: false,
userRoles: false,
domainMaxCount: 1,
externalLdap: false,
privateDockerRegistry: false,
branding: false,
support: false,
directoryConfig: false,
mailboxMaxCount: 5,
emailPremium: false
support: false
};
// attempt to load feature cache in case appstore would be down
@@ -230,6 +232,110 @@ function unpurchaseApp(appId, data, callback) {
});
}
function sendAliveStatus(callback) {
callback = callback || NOOP_CALLBACK;
let allSettings, allDomains, mailDomains, loginEvents, userCount, groupCount;
async.series([
function (callback) {
settings.getAll(function (error, result) {
if (error) return callback(error);
allSettings = result;
callback();
});
},
function (callback) {
domains.getAll(function (error, result) {
if (error) return callback(error);
allDomains = result;
callback();
});
},
function (callback) {
mail.getDomains(function (error, result) {
if (error) return callback(error);
mailDomains = result;
callback();
});
},
function (callback) {
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
if (error) return callback(error);
loginEvents = result;
callback();
});
},
function (callback) {
users.count(function (error, result) {
if (error) return callback(error);
userCount = result;
callback();
});
},
function (callback) {
groups.count(function (error, result) {
if (error) return callback(error);
groupCount = result;
callback();
});
}
], function (error) {
if (error) return callback(error);
var backendSettings = {
backupConfig: {
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: allDomains.length,
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
userCount: userCount,
groupCount: groupCount,
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
sysinfoProvider: allSettings[settings.SYSINFO_CONFIG_KEY].provider
};
var data = {
version: constants.VERSION,
adminFqdn: settings.adminFqdn(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/alive`;
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
});
});
});
}
function getBoxUpdate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -245,17 +351,17 @@ function getBoxUpdate(options, callback) {
automatic: options.automatic
};
superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) {
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode === 204) return callback(null, null); // no update
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
var updateInfo = result.body;
if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) {
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Update version invalid or is a downgrade: %s %s', result.statusCode, result.text)));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
}
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
@@ -288,7 +394,7 @@ function getAppUpdate(app, options, callback) {
automatic: options.automatic
};
superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) {
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
@@ -353,8 +459,8 @@ function trackBeginSetup() {
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_begin`;
superagent.post(url).send({}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return debug(`trackBeginSetup: ${error.message}`);
if (result.statusCode !== 200) return debug(`trackBeginSetup: ${result.statusCode} ${error.message}`);
if (error && !error.response) return console.error(error.message);
if (result.statusCode !== 200) return console.error(error.message);
});
}
@@ -365,8 +471,8 @@ function trackFinishedSetup(domain) {
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_finished`;
superagent.post(url).send({ domain }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return debug(`trackFinishedSetup: ${error.message}`);
if (result.statusCode !== 200) return debug(`trackFinishedSetup: ${result.statusCode} ${error.message}`);
if (error && !error.response) return console.error(error.message);
if (result.statusCode !== 200) return console.error(error.message);
});
}
@@ -410,55 +516,26 @@ function createTicket(info, auditSource, callback) {
apps.get(info.appId, callback);
}
function enableSshIfNeeded(callback) {
if (!info.enableSshSupport) return callback();
support.enableRemoteSupport(true, auditSource, function (error) {
// ensure we can at least get the ticket through
if (error) debug('Unable to enable SSH support.', error);
callback();
});
}
getCloudronToken(function (error, token) {
if (error) return callback(error);
enableSshIfNeeded(function (error) {
collectAppInfoIfNeeded(function (error, result) {
if (error) return callback(error);
if (result) info.app = result;
collectAppInfoIfNeeded(function (error, app) {
if (error) return callback(error);
if (app) info.app = app;
let url = settings.apiServerOrigin() + '/api/v1/ticket';
info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets
info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets
var req = superagent.post(`${settings.apiServerOrigin()}/api/v1/ticket`)
.query({ accessToken: token })
.timeout(30 * 1000);
superagent.post(url).query({ accessToken: token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
// either send as JSON through body or as multipart, depending on attachments
if (info.app) {
req.field('infoJSON', JSON.stringify(info));
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
apps.getLocalLogfilePaths(info.app).forEach(function (filePath) {
var logs = safe.child_process.execSync(`tail --lines=1000 ${filePath}`);
if (logs) req.attach(path.basename(filePath), logs, path.basename(filePath));
});
} else {
req.send(info);
}
req.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
callback(null, { message: `An email was sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` });
});
callback(null, { message: `An email for sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` });
});
});
});
@@ -474,7 +551,7 @@ function getApps(callback) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/apps`;
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(30 * 1000).end(function (error, result) {
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(10 * 1000).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.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
@@ -509,7 +586,7 @@ function getAppVersion(appId, version, callback) {
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
if (version !== 'latest') url += `/versions/${version}`;
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
superagent.get(url).query({ accessToken: token }).timeout(10 * 1000).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.INVALID_CREDENTIALS));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
+59 -79
View File
@@ -6,6 +6,7 @@ exports = module.exports = {
run: run,
// exported for testing
_reserveHttpPort: reserveHttpPort,
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_createAppDir: createAppDir,
@@ -16,7 +17,10 @@ exports = module.exports = {
_waitForDnsPropagation: waitForDnsPropagation
};
const appdb = require('./appdb.js'),
require('supererror')({ splatchError: true });
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
@@ -32,15 +36,14 @@ const appdb = require('./appdb.js'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
iputils = require('./iputils.js'),
manifestFormat = require('cloudron-manifestformat'),
net = require('net'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
services = require('./services.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
@@ -88,16 +91,22 @@ function updateApp(app, values, callback) {
});
}
function allocateContainerIp(app, callback) {
function reserveHttpPort(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
async.retry({ times: 10 }, function (retryCallback) {
const iprange = iputils.intFromIp('172.18.20.255') - iputils.intFromIp('172.18.16.1');
let rnd = Math.floor(Math.random() * iprange);
const containerIp = iputils.ipFromInt(iputils.intFromIp('172.18.16.1') + rnd);
updateApp(app, { containerIp }, retryCallback);
}, callback);
let server = net.createServer();
server.listen(0, function () {
let port = server.address().port;
updateApp(app, { httpPort: port }, function (error) {
server.close(function (/* closeError */) {
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Failed to allocate http port ${port}: ${error.message}`));
callback(null);
});
});
});
}
function configureReverseProxy(app, callback) {
@@ -341,9 +350,9 @@ function registerSubdomains(app, overwrite, callback) {
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
const allDomains = [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains);
const allDomains = [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains);
debugApp(app, `registerSubdomain: Will register ${JSON.stringify(allDomains)}`);
debug(`registerSubdomain: Will register ${JSON.stringify(allDomains)}`);
async.eachSeries(allDomains, function (domain, iteratorDone) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
@@ -363,7 +372,7 @@ function registerSubdomains(app, overwrite, callback) {
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === BoxError.BUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
debugApp(app, 'registerSubdomains: Upsert error. Will retry.', error.message);
debug('registerSubdomains: Upsert error. Will retry.', error.message);
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
}
@@ -394,7 +403,7 @@ function unregisterSubdomains(app, allDomains, callback) {
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, null);
if (error && (error.reason === BoxError.SBUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
debugApp(app, 'registerSubdomains: Remove error. Will retry.', error.message);
debug('registerSubdomains: Remove error. Will retry.', error.message);
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
}
@@ -424,8 +433,8 @@ function waitForDnsPropagation(app, callback) {
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 and aliasDomains, if any
async.eachSeries(app.alternateDomains.concat(app.aliasDomains), function (domain, iteratorCallback) {
// now wait for alternateDomains, if any
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
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 }));
@@ -444,7 +453,7 @@ function moveDataDir(app, targetDir, callback) {
let resolvedSourceDir = apps.getDataDir(app, app.dataDir);
let resolvedTargetDir = apps.getDataDir(app, targetDir);
debugApp(app, `moveDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
debug(`moveDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
if (resolvedSourceDir === resolvedTargetDir) return callback();
@@ -477,8 +486,6 @@ function downloadImage(manifest, callback) {
}
function startApp(app, callback){
debugApp(app, 'startApp: starting container');
if (app.runState === apps.RSTATE_STOPPED) return callback();
docker.startContainer(app.id, callback);
@@ -512,7 +519,7 @@ function install(app, args, progressCallback, callback) {
addonsToRemove = app.manifest.addons;
}
services.teardownAddons(app, addonsToRemove, next);
addons.teardownAddons(app, addonsToRemove, next);
},
function deleteAppDirIfNeeded(done) {
@@ -527,8 +534,7 @@ function install(app, args, progressCallback, callback) {
docker.deleteImage(oldManifest, done);
},
// allocating container ip here, lets the users "repair" an app if allocation fails at appdb.add time
allocateContainerIp.bind(null, app),
reserveHttpPort.bind(null, app),
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
@@ -546,24 +552,24 @@ function install(app, args, progressCallback, callback) {
if (!restoreConfig) {
async.series([
progressCallback.bind(null, { percent: 60, message: 'Setting up addons' }),
services.setupAddons.bind(null, app, app.manifest.addons),
addons.setupAddons.bind(null, app, app.manifest.addons),
], next);
} else if (!restoreConfig.backupId) { // in-place import
async.series([
progressCallback.bind(null, { percent: 60, message: 'Importing addons in-place' }),
services.setupAddons.bind(null, app, app.manifest.addons),
services.clearAddons.bind(null, app, _.omit(app.manifest.addons, 'localstorage')),
services.restoreAddons.bind(null, app, app.manifest.addons),
addons.setupAddons.bind(null, app, app.manifest.addons),
addons.clearAddons.bind(null, app, _.omit(app.manifest.addons, 'localstorage')),
addons.restoreAddons.bind(null, app, app.manifest.addons),
], next);
} else {
async.series([
progressCallback.bind(null, { percent: 65, message: 'Download backup and restoring addons' }),
services.setupAddons.bind(null, app, app.manifest.addons),
services.clearAddons.bind(null, app, app.manifest.addons),
addons.setupAddons.bind(null, app, app.manifest.addons),
addons.clearAddons.bind(null, app, app.manifest.addons),
backups.downloadApp.bind(null, app, restoreConfig, (progress) => {
progressCallback({ percent: 65, message: progress.message });
}),
services.restoreAddons.bind(null, app, app.manifest.addons)
addons.restoreAddons.bind(null, app, app.manifest.addons)
], next);
}
},
@@ -598,7 +604,7 @@ function backup(app, args, progressCallback, callback) {
async.series([
progressCallback.bind(null, { percent: 10, message: 'Backing up' }),
backups.backupApp.bind(null, app, { snapshotOnly: !!args.snapshotOnly }, (progress) => {
backups.backupApp.bind(null, app, { /* options */ }, (progress) => {
progressCallback({ percent: 30, message: progress.message });
}),
@@ -626,7 +632,7 @@ function create(app, args, progressCallback, callback) {
// FIXME: re-setup addons only because sendmail addon to re-inject env vars on mailboxName change
progressCallback.bind(null, { percent: 30, message: 'Setting up addons' }),
services.setupAddons.bind(null, app, app.manifest.addons),
addons.setupAddons.bind(null, app, app.manifest.addons),
progressCallback.bind(null, { percent: 60, message: 'Creating container' }),
createContainer.bind(null, app),
@@ -663,12 +669,6 @@ function changeLocation(app, args, progressCallback, callback) {
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
});
if (oldConfig.aliasDomains) {
obsoleteDomains = obsoleteDomains.concat(oldConfig.aliasDomains.filter(function (o) {
return !app.aliasDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
}));
}
if (locationChanged) obsoleteDomains.push({ subdomain: oldConfig.location, domain: oldConfig.domain });
if (obsoleteDomains.length === 0) return next();
@@ -681,7 +681,7 @@ function changeLocation(app, args, progressCallback, callback) {
// re-setup addons since they rely on the app's fqdn (e.g oauth)
progressCallback.bind(null, { percent: 50, message: 'Setting up addons' }),
services.setupAddons.bind(null, app, app.manifest.addons),
addons.setupAddons.bind(null, app, app.manifest.addons),
progressCallback.bind(null, { percent: 60, message: 'Creating container' }),
createContainer.bind(null, app),
@@ -723,7 +723,7 @@ function migrateDataDir(app, args, progressCallback, callback) {
// re-setup addons since this creates the localStorage volume
progressCallback.bind(null, { percent: 50, message: 'Setting up addons' }),
services.setupAddons.bind(null, _.extend({}, app, { dataDir: newDataDir }), app.manifest.addons),
addons.setupAddons.bind(null, _.extend({}, app, { dataDir: newDataDir }), app.manifest.addons),
progressCallback.bind(null, { percent: 60, message: 'Moving data dir' }),
moveDataDir.bind(null, app, newDataDir),
@@ -740,8 +740,7 @@ function migrateDataDir(app, args, progressCallback, callback) {
debugApp(app, 'error migrating data dir : %s', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback();
callback(null);
});
}
@@ -756,6 +755,7 @@ function configure(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
reserveHttpPort.bind(null, app),
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
@@ -768,7 +768,7 @@ function configure(app, args, progressCallback, callback) {
// re-setup addons since they rely on the app's fqdn (e.g oauth)
progressCallback.bind(null, { percent: 50, message: 'Setting up addons' }),
services.setupAddons.bind(null, app, app.manifest.addons),
addons.setupAddons.bind(null, app, app.manifest.addons),
progressCallback.bind(null, { percent: 60, message: 'Creating container' }),
createContainer.bind(null, app),
@@ -785,11 +785,11 @@ function configure(app, args, progressCallback, callback) {
debugApp(app, 'error reconfiguring : %s', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback();
callback(null);
});
}
// nginx configuration is skipped because app.httpPort is expected to be available
function update(app, args, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof args, 'object');
@@ -801,10 +801,7 @@ function update(app, args, progressCallback, callback) {
// app does not want these addons anymore
// FIXME: this does not handle option changes (like multipleDatabases)
const unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
const httpPathsChanged = app.manifest.httpPaths !== updateConfig.manifest.httpPaths;
const httpPortChanged = app.manifest.httpPort !== updateConfig.manifest.httpPort;
const proxyAuthChanged = !_.isEqual(safe.query(app.manifest, 'addons.proxyAuth'), safe.query(updateConfig.manifest, 'addons.proxyAuth'));
var unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
async.series([
// this protects against the theoretical possibility of an app being marked for update from
@@ -843,7 +840,7 @@ function update(app, args, progressCallback, callback) {
},
// only delete unused addons after backup
services.teardownAddons.bind(null, app, unusedAddons),
addons.teardownAddons.bind(null, app, unusedAddons),
// free unused ports
function (next) {
@@ -855,7 +852,7 @@ function update(app, args, progressCallback, callback) {
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) debugApp(app, 'update: portbinding does not exist in database', error);
if (error && error.reason === BoxError.NOT_FOUND) console.error('Portbinding does not exist in database.');
else if (error) return next(error);
// also delete from app object for further processing (the db is updated in the next step)
@@ -871,21 +868,14 @@ function update(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 45, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
progressCallback.bind(null, { percent: 60, message: 'Updating addons' }),
services.setupAddons.bind(null, app, updateConfig.manifest.addons),
progressCallback.bind(null, { percent: 70, message: 'Updating addons' }),
addons.setupAddons.bind(null, app, updateConfig.manifest.addons),
progressCallback.bind(null, { percent: 70, message: 'Creating container' }),
progressCallback.bind(null, { percent: 80, message: 'Creating container' }),
createContainer.bind(null, app),
startApp.bind(null, app),
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
function (next) {
if (!httpPathsChanged && !proxyAuthChanged && !httpPortChanged) return next();
configureReverseProxy(app, next);
},
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() })
], function seriesDone(error) {
@@ -909,16 +899,13 @@ function start(app, args, progressCallback, callback) {
async.series([
progressCallback.bind(null, { percent: 10, message: 'Starting app services' }),
services.startAppServices.bind(null, app),
addons.startAppServices.bind(null, app),
progressCallback.bind(null, { percent: 35, message: 'Starting container' }),
docker.startContainer.bind(null, app.id),
progressCallback.bind(null, { percent: 60, message: 'Adding collectd profile' }),
addCollectdProfile.bind(null, app),
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
progressCallback.bind(null, { percent: 80, message: 'Configuring reverse proxy' }),
progressCallback.bind(null, { percent: 60, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
@@ -943,10 +930,7 @@ function stop(app, args, progressCallback, callback) {
docker.stopContainers.bind(null, app.id),
progressCallback.bind(null, { percent: 50, message: 'Stopping app services' }),
services.stopAppServices.bind(null, app),
progressCallback.bind(null, { percent: 80, message: 'Removing collectd profile' }),
removeCollectdProfile.bind(null, app),
addons.stopAppServices.bind(null, app),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
@@ -992,20 +976,18 @@ function uninstall(app, args, progressCallback, callback) {
deleteContainers.bind(null, app, {}),
progressCallback.bind(null, { percent: 30, message: 'Teardown addons' }),
services.teardownAddons.bind(null, app, app.manifest.addons),
addons.teardownAddons.bind(null, app, app.manifest.addons),
progressCallback.bind(null, { percent: 40, message: 'Cleanup file manager' }),
progressCallback.bind(null, { percent: 50, message: 'Deleting app data directory' }),
progressCallback.bind(null, { percent: 40, message: 'Deleting app data directory' }),
deleteAppDir.bind(null, app, { removeDirectory: true }),
progressCallback.bind(null, { percent: 60, message: 'Deleting image' }),
progressCallback.bind(null, { percent: 50, message: 'Deleting image' }),
docker.deleteImage.bind(null, app.manifest),
progressCallback.bind(null, { percent: 70, message: 'Unregistering domains' }),
unregisterSubdomains.bind(null, app, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains).concat(app.aliasDomains)),
progressCallback.bind(null, { percent: 60, message: 'Unregistering domains' }),
unregisterSubdomains.bind(null, app, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains)),
progressCallback.bind(null, { percent: 80, message: 'Cleanup icon' }),
progressCallback.bind(null, { percent: 70, message: 'Cleanup icon' }),
removeIcon.bind(null, app),
progressCallback.bind(null, { percent: 90, message: 'Cleanup logs' }),
@@ -1061,8 +1043,6 @@ function run(appId, args, progressCallback, callback) {
return stop(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_RESTART:
return restart(app, args, progressCallback, callback);
case apps.ISTATE_INSTALLED: // can only happen when we have a bug in our code while testing/development
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }, callback);
default:
debugApp(app, 'apptask launched with invalid command');
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Unknown install command in apptask:' + app.installationState));
+3 -12
View File
@@ -1,7 +1,7 @@
'use strict';
exports = module.exports = {
scheduleTask
scheduleTask: scheduleTask
};
let assert = require('assert'),
@@ -12,8 +12,6 @@ let assert = require('assert'),
safe = require('safetydance'),
path = require('path'),
paths = require('./paths.js'),
scheduler = require('./scheduler.js'),
services = require('./services.js'),
tasks = require('./tasks.js');
let gActiveTasks = { }; // indexed by app id
@@ -37,10 +35,9 @@ function initializeSync() {
}
// callback is called when task is finished
function scheduleTask(appId, taskId, options, callback) {
function scheduleTask(appId, taskId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (!gInitialized) initializeSync();
@@ -71,17 +68,11 @@ function scheduleTask(appId, taskId, options, callback) {
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
scheduler.suspendJobs(appId);
tasks.startTask(taskId, Object.assign(options, { logFile }), function (error, result) {
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */ }, function (error, result) {
callback(error, result);
delete gActiveTasks[appId];
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
// post app task hooks
services.rebuildService('sftp', error => { if (error) debug('Unable to rebuild sftp:', error); });
scheduler.resumeJobs(appId);
});
}
+88 -97
View File
@@ -1,36 +1,37 @@
'use strict';
exports = module.exports = {
testConfig,
testProviderConfig,
testConfig: testConfig,
testProviderConfig: testProviderConfig,
getByIdentifierAndStatePaged,
get,
get: get,
startBackupTask,
startBackupTask: startBackupTask,
ensureBackup: ensureBackup,
restore,
restore: restore,
backupApp,
downloadApp,
backupApp: backupApp,
downloadApp: downloadApp,
backupBoxAndApps,
backupBoxAndApps: backupBoxAndApps,
upload,
upload: upload,
startCleanupTask,
cleanup,
cleanupCacheFilesSync,
startCleanupTask: startCleanupTask,
cleanup: cleanup,
cleanupCacheFilesSync: cleanupCacheFilesSync,
injectPrivateFields,
removePrivateFields,
injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields,
checkConfiguration,
checkConfiguration: checkConfiguration,
configureCollectd,
configureCollectd: configureCollectd,
generateEncryptionKeysSync,
generateEncryptionKeysSync: generateEncryptionKeysSync,
BACKUP_IDENTIFIER_BOX: 'box',
@@ -48,14 +49,14 @@ exports = module.exports = {
_applyBackupRetentionPolicy: applyBackupRetentionPolicy
};
const apps = require('./apps.js'),
var addons = require('./addons.js'),
apps = require('./apps.js'),
async = require('async'),
assert = require('assert'),
backupdb = require('./backupdb.js'),
BoxError = require('./boxerror.js'),
collectd = require('./collectd.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
crypto = require('crypto'),
database = require('./database.js'),
DataLayout = require('./datalayout.js'),
@@ -71,7 +72,6 @@ const apps = require('./apps.js'),
progressStream = require('progress-stream'),
safe = require('safetydance'),
shell = require('./shell.js'),
services = require('./services.js'),
settings = require('./settings.js'),
syncer = require('./syncer.js'),
tar = require('tar-fs'),
@@ -105,7 +105,6 @@ function api(provider) {
case 'exoscale-sos': return require('./storage/s3.js');
case 'wasabi': return require('./storage/s3.js');
case 'scaleway-objectstorage': return require('./storage/s3.js');
case 'backblaze-b2': return require('./storage/s3.js');
case 'linode-objectstorage': return require('./storage/s3.js');
case 'ovh-objectstorage': return require('./storage/s3.js');
case 'noop': return require('./storage/noop.js');
@@ -143,8 +142,8 @@ function testConfig(backupConfig, callback) {
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BoxError(BoxError.BAD_FIELD, 'unknown format', { field: 'format' }));
const job = safe.safeCall(function () { return new CronJob(backupConfig.schedulePattern); });
if (!job) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid schedule pattern', { field: 'schedulePattern' }));
// remember to adjust the cron ensureBackup task interval accordingly
if (backupConfig.intervalSecs < 6 * 60 * 60) return callback(new BoxError(BoxError.BAD_FIELD, 'Interval must be atleast 6 hours', { field: 'intervalSecs' }));
if ('password' in backupConfig) {
if (typeof backupConfig.password !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'password must be a string', { field: 'password' }));
@@ -380,11 +379,9 @@ function createReadStream(sourceFile, encryption) {
stream.on('error', function (error) {
debug(`createReadStream: read stream error at ${sourceFile}`, error);
ps.emit('error', new BoxError(BoxError.FS_ERROR, `Error reading ${sourceFile}: ${error.message} ${error.code}`));
ps.emit('error', new BoxError(BoxError.FS_ERROR, `Error reading ${sourceFile}: ${error.message}`));
});
stream.on('open', () => ps.emit('open'));
if (encryption) {
let encryptStream = new EncryptStream(encryption);
@@ -517,19 +514,18 @@ function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
progressCallback({ message: `Adding ${task.path}` + (retryCount > 1 ? ` (Try ${retryCount})` : '') });
debug(`Adding ${task.path} position ${task.position} try ${retryCount}`);
var stream = createReadStream(dataLayout.toLocalPath('./' + task.path), backupConfig.encryption);
stream.on('error', (error) => retryCallback(error.message.includes('ENOENT') ? null : error)); // ignore error if file disappears
stream.on('error', function (error) {
debug(`read stream error for ${task.path}: ${error.message}`);
retryCallback();
}); // ignore error if file disappears
stream.on('progress', function (progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: `Uploading ${task.path}` }); // 0M@0MBps looks wrong
progressCallback({ message: `Uploading ${task.path}: ${transferred}M@${speed}MBps` }); // 0M@0MBps looks wrong
});
// only create the destination path when we have confirmation that the source is available. otherwise, we end up with
// files owned as 'root' and the cp later will fail
stream.on('open', function () {
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
debug(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
retryCallback(error);
});
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
debug(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
retryCallback(error);
});
}
}, iteratorCallback);
@@ -851,23 +847,15 @@ function runBackupUpload(uploadConfig, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
const { backupId, backupConfig, dataLayout, progressTag } = uploadConfig;
const { backupId, format, dataLayout, progressTag } = uploadConfig;
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof progressTag, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
let result = ''; // the script communicates error result as a string
// https://stackoverflow.com/questions/48387040/node-js-recommended-max-old-space-size
const envCopy = Object.assign({}, process.env);
if (backupConfig.memoryLimit && backupConfig.memoryLimit >= 2*1024*1024*1024) {
const heapSize = Math.min((backupConfig.memoryLimit/1024/1024) - 256, 8192);
debug(`runBackupUpload: adjusting heap size to ${heapSize}M`);
envCopy.NODE_OPTIONS = `--max-old-space-size=${heapSize}`;
}
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, backupConfig.format, dataLayout.toString() ], { env: envCopy, preserveEnv: true, ipc: true }, function (error) {
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataLayout.toString() ], { preserveEnv: true, ipc: true }, function (error) {
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Backuptask crashed'));
} else if (error && error.code === 50) { // exited with error
@@ -912,13 +900,9 @@ function snapshotBox(progressCallback, callback) {
progressCallback({ message: 'Snapshotting box' });
const startTime = new Date();
database.exportToFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
debug(`snapshotBox: took ${(new Date() - startTime)/1000} seconds`);
return callback();
});
}
@@ -928,6 +912,8 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var startTime = new Date();
snapshotBox(progressCallback, function (error) {
if (error) return callback(error);
@@ -936,29 +922,26 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
const uploadConfig = {
backupId: 'snapshot/box',
backupConfig,
format: backupConfig.format,
dataLayout: new DataLayout(boxDataDir, []),
progressTag: 'box'
};
progressCallback({ message: 'Uploading box snapshot' });
const startTime = new Date();
runBackupUpload(uploadConfig, progressCallback, function (error) {
if (error) return callback(error);
debug(`uploadBoxSnapshot: took ${(new Date() - startTime)/1000} seconds`);
debug('uploadBoxSnapshot: time: %s secs', (new Date() - startTime)/1000);
setSnapshotInfo('box', { timestamp: new Date().toISOString(), format: backupConfig.format }, callback);
});
});
}
function rotateBoxBackup(backupConfig, tag, options, appBackupIds, progressCallback, callback) {
function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert(Array.isArray(appBackupIds));
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
@@ -989,7 +972,7 @@ function rotateBoxBackup(backupConfig, tag, options, appBackupIds, progressCallb
copy.on('done', function (copyBackupError) {
const state = copyBackupError ? exports.BACKUP_STATE_ERROR : exports.BACKUP_STATE_NORMAL;
backupdb.update(backupId, { preserveSecs: options.preserveSecs || 0, state }, function (error) {
backupdb.update(backupId, { state }, function (error) {
if (copyBackupError) return callback(copyBackupError);
if (error) return callback(error);
@@ -1001,10 +984,9 @@ function rotateBoxBackup(backupConfig, tag, options, appBackupIds, progressCallb
});
}
function backupBoxWithAppBackupIds(appBackupIds, tag, options, progressCallback, callback) {
function backupBoxWithAppBackupIds(appBackupIds, tag, progressCallback, callback) {
assert(Array.isArray(appBackupIds));
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
@@ -1014,7 +996,7 @@ function backupBoxWithAppBackupIds(appBackupIds, tag, options, progressCallback,
uploadBoxSnapshot(backupConfig, progressCallback, function (error) {
if (error) return callback(error);
rotateBoxBackup(backupConfig, tag, options, appBackupIds, progressCallback, callback);
rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, callback);
});
});
}
@@ -1038,18 +1020,15 @@ function snapshotApp(app, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
const startTime = new Date();
progressCallback({ message: `Snapshotting app ${app.fqdn}` });
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(app))) {
return callback(new BoxError(BoxError.FS_ERROR, 'Error creating config.json: ' + safe.error.message));
}
services.backupAddons(app, app.manifest.addons, function (error) {
addons.backupAddons(app, app.manifest.addons, function (error) {
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
debugApp(app, `snapshotApp: took ${(new Date() - startTime)/1000} seconds`);
return callback(null);
});
}
@@ -1062,8 +1041,6 @@ function rotateAppBackup(backupConfig, app, tag, options, progressCallback, call
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
const startTime = new Date();
var snapshotInfo = getSnapshotInfo(app.id);
var manifest = snapshotInfo.restoreConfig ? snapshotInfo.restoreConfig.manifest : snapshotInfo.manifest; // compat
@@ -1096,7 +1073,7 @@ function rotateAppBackup(backupConfig, app, tag, options, progressCallback, call
if (copyBackupError) return callback(copyBackupError);
if (error) return callback(error);
debug(`Rotated app backup of ${app.id} successfully to id ${backupId}. Took ${(new Date() - startTime)/1000} seconds`);
debug(`Rotated app backup of ${app.id} successfully to id ${backupId}`);
callback(null, backupId);
});
@@ -1110,6 +1087,8 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var startTime = new Date();
snapshotApp(app, progressCallback, function (error) {
if (error) return callback(error);
@@ -1123,17 +1102,15 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
const uploadConfig = {
backupId,
backupConfig,
format: backupConfig.format,
dataLayout,
progressTag: app.fqdn
};
const startTime = new Date();
runBackupUpload(uploadConfig, progressCallback, function (error) {
if (error) return callback(error);
debugApp(app, `uploadAppSnapshot: ${backupId} done. ${(new Date() - startTime)/1000} seconds`);
debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000);
setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), manifest: app.manifest, format: backupConfig.format }, callback);
});
@@ -1175,8 +1152,6 @@ function backupApp(app, options, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (options.snapshotOnly) return snapshotApp(app, progressCallback, callback);
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
debug(`backupApp - Backing up ${app.fqdn} with tag ${tag}`);
@@ -1185,8 +1160,7 @@ function backupApp(app, options, progressCallback, callback) {
}
// this function expects you to have a lock. Unlike other progressCallback this also has a progress field
function backupBoxAndApps(options, progressCallback, callback) {
assert.strictEqual(typeof options, 'object');
function backupBoxAndApps(progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
@@ -1207,14 +1181,13 @@ function backupBoxAndApps(options, progressCallback, callback) {
return iteratorCallback(null, null); // nothing to backup
}
const startTime = new Date();
backupAppWithTag(app, tag, options, (progress) => progressCallback({ percent: percent, message: progress.message }), function (error, backupId) {
backupAppWithTag(app, tag, { /* options */ }, (progress) => progressCallback({ percent: percent, message: progress.message }), function (error, backupId) {
if (error) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
}
debugApp(app, `Backed up. Took ${(new Date() - startTime)/1000} seconds`);
debugApp(app, 'Backed up');
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
@@ -1226,7 +1199,7 @@ function backupBoxAndApps(options, progressCallback, callback) {
progressCallback({ percent: percent, message: 'Backing up system data' });
percent += step;
backupBoxWithAppBackupIds(backupIds, tag, options, (progress) => progressCallback({ percent: percent, message: progress.message }), callback);
backupBoxWithAppBackupIds(backupIds, tag, (progress) => progressCallback({ percent: percent, message: progress.message }), callback);
});
});
}
@@ -1235,30 +1208,49 @@ function startBackupTask(auditSource, callback) {
let error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new BoxError(BoxError.BAD_STATE, `Cannot backup now: ${error.message}`));
settings.getBackupConfig(function (error, backupConfig) {
tasks.add(tasks.TASK_BACKUP, [ ], function (error, taskId) {
if (error) return callback(error);
const memoryLimit = 'memoryLimit' in backupConfig ? Math.max(backupConfig.memoryLimit/1024/1024, 400) : 400;
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
tasks.add(tasks.TASK_BACKUP, [ { /* options */ } ], function (error, taskId) {
tasks.startTask(taskId, { timeout: 12 * 60 * 60 * 1000 /* 12 hours */ }, function (error, backupId) {
locker.unlock(locker.OP_FULL_BACKUP);
const errorMessage = error ? error.message : '';
const timedOut = error ? error.code === tasks.ETIMEOUT : false;
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId, errorMessage, timedOut, backupId });
});
callback(null, taskId);
});
}
function ensureBackup(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
debug('ensureBackup: %j', auditSource);
getByIdentifierAndStatePaged(exports.BACKUP_IDENTIFIER_BOX, exports.BACKUP_STATE_NORMAL, 1, 1, function (error, backups) {
if (error) {
debug('Unable to list backups', error);
return callback(error);
}
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < (backupConfig.intervalSecs - 3600) * 1000)) { // adjust 1 hour
debug('Previous backup was %j, no need to backup now', backups[0]);
return callback(null);
}
tasks.startTask(taskId, { timeout: 24 * 60 * 60 * 1000 /* 24 hours */, nice: 15, memoryLimit }, function (error, backupId) {
locker.unlock(locker.OP_FULL_BACKUP);
const errorMessage = error ? error.message : '';
const timedOut = error ? error.code === tasks.ETIMEOUT : false;
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId, errorMessage, timedOut, backupId });
});
callback(null, taskId);
startBackupTask(auditSource, callback);
});
});
}
// backups must be descending in creationTime
function applyBackupRetentionPolicy(backups, policy, referencedBackupIds) {
assert(Array.isArray(backups));
assert.strictEqual(typeof policy, 'object');
@@ -1296,13 +1288,12 @@ function applyBackupRetentionPolicy(backups, policy, referencedBackupIds) {
let lastPeriod = null, keptSoFar = 0;
for (const backup of backups) {
if (backup.discardReason) continue; // already discarded for some reason
if (backup.keepReason && backup.keepReason !== 'reference') continue; // kept for some other reason
if (backup.discardReason || backup.keepReason) continue; // already kept or discarded for some reason
const period = moment(backup.creationTime).format(KEEP_FORMATS[format]);
if (period === lastPeriod) continue; // already kept for this period
lastPeriod = period;
backup.keepReason = backup.keepReason ? `${backup.keepReason}+${format}` : format;
backup.keepReason = format;
if (++keptSoFar === n) break;
}
}
@@ -1536,9 +1527,9 @@ function checkConfiguration(callback) {
let message = '';
if (backupConfig.provider === 'noop') {
message = 'Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://docs.cloudron.io/backups/#storage-providers for more information.';
message = 'Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://cloudron.io/documentation/backups/#storage-providers for more information.';
} else if (backupConfig.provider === 'filesystem' && !backupConfig.externalDisk) {
message = 'Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://docs.cloudron.io/backups/#storage-providers for storing backups in an external location.';
message = 'Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://cloudron.io/documentation/backups/#storage-providers for storing backups in an external location.';
}
callback(null, message);
-2
View File
@@ -50,7 +50,6 @@ BoxError.FS_ERROR = 'FileSystem Error';
BoxError.INACTIVE = 'Inactive';
BoxError.INTERNAL_ERROR = 'Internal Error';
BoxError.INVALID_CREDENTIALS = 'Invalid Credentials';
BoxError.IPTABLES_ERROR = 'IPTables Error';
BoxError.LICENSE_ERROR = 'License Error';
BoxError.LOGROTATE_ERROR = 'Logrotate Error';
BoxError.MAIL_ERROR = 'Mail Error';
@@ -93,7 +92,6 @@ BoxError.toHttpError = function (error) {
case BoxError.MAIL_ERROR:
case BoxError.DOCKER_ERROR:
case BoxError.ADDONS_ERROR:
case BoxError.IPTABLES_ERROR:
return new HttpError(424, error);
case BoxError.DATABASE_ERROR:
case BoxError.INTERNAL_ERROR:
-18
View File
@@ -1,18 +0,0 @@
'use strict';
exports = module.exports = {
renderFooter
};
const assert = require('assert'),
constants = require('./constants.js');
function renderFooter(footer) {
assert.strictEqual(typeof footer, 'string');
const year = new Date().getFullYear();
return footer.replace(/%YEAR%/g, year)
.replace(/%VERSION%/g, constants.VERSION);
}
+18 -18
View File
@@ -139,7 +139,7 @@ Acme2.prototype.updateContact = function (registrationUri, callback) {
const that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to update contact. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
debug(`updateContact: contact of user updated to ${that.email}`);
@@ -160,7 +160,7 @@ Acme2.prototype.registerUser = function (callback) {
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
if (error) return callback(error);
// 200 if already exists. 201 for new accounts
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.statusCode} ${JSON.stringify(result.body)}`));
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
debug(`registerUser: user registered keyid: ${result.headers.location}`);
@@ -185,8 +185,8 @@ Acme2.prototype.newOrder = function (domain, callback) {
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
if (error) return callback(error);
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending new order: ${result.body.detail}`));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to send new order. Expecting 201, got ${result.statusCode} ${JSON.stringify(result.body)}`));
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending signed request: ${result.body.detail}`));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('newOrder: created order %s %j', domain, result.body);
@@ -259,7 +259,7 @@ Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to notify challenge. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
callback();
});
@@ -313,7 +313,7 @@ Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, cal
this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) {
if (error) return callback(error);
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to sign certificate. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
return callback(null);
});
@@ -362,7 +362,7 @@ Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
that.postAsGet(certUrl, function (error, result) {
if (error) return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error when downloading certificate: ${error.message}`));
if (result.statusCode === 202) return retryCallback(new BoxError(BoxError.TRY_AGAIN, 'Retry downloading certificate'));
if (result.statusCode !== 200) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to get cert. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`));
if (result.statusCode !== 200) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
const fullChainPem = result.body; // buffer
@@ -586,34 +586,34 @@ Acme2.prototype.getDirectory = function (callback) {
});
};
Acme2.prototype.getCertificate = function (vhost, domain, callback) {
assert.strictEqual(typeof vhost, 'string');
Acme2.prototype.getCertificate = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`getCertificate: start acme flow for ${vhost} from ${this.caDirectory}`);
debug(`getCertificate: start acme flow for ${hostname} from ${this.caDirectory}`);
if (vhost !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
vhost = domains.makeWildcard(vhost);
debug(`getCertificate: will get wildcard cert for ${vhost}`);
if (hostname !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
hostname = domains.makeWildcard(hostname);
debug(`getCertificate: will get wildcard cert for ${hostname}`);
}
const that = this;
this.getDirectory(function (error) {
if (error) return callback(error);
that.acmeFlow(vhost, domain, function (error) {
that.acmeFlow(hostname, domain, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
const certName = vhost.replace('*.', '_.');
const certName = hostname.replace('*.', '_.');
callback(null, path.join(outdir, `${certName}.cert`), path.join(outdir, `${certName}.key`));
});
});
};
function getCertificate(vhost, domain, options, callback) {
assert.strictEqual(typeof vhost, 'string'); // this can also be a wildcard domain (for alias domains)
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -623,6 +623,6 @@ function getCertificate(vhost, domain, options, callback) {
debug(`getCertificate: attempt ${attempt++}`);
let acme = new Acme2(options || { });
acme.getCertificate(vhost, domain, retryCallback);
acme.getCertificate(hostname, domain, retryCallback);
}, callback);
}
+22
View File
@@ -0,0 +1,22 @@
'use strict';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'caas'
};
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', hostname);
return callback(null, '', '');
}
+22
View File
@@ -0,0 +1,22 @@
'use strict';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'fallback'
};
var assert = require('assert'),
debug = require('debug')('box:cert/fallback.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', hostname);
return callback(null, '', '');
}
+24
View File
@@ -0,0 +1,24 @@
'use strict';
// -------------------------------------------
// This file just describes the interface
//
// New backends can start from here
// -------------------------------------------
exports = module.exports = {
getCertificate: getCertificate
};
var assert = require('assert'),
BoxError = require('../boxerror.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
return callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'getCertificate is not implemented'));
}
+65 -109
View File
@@ -1,34 +1,34 @@
'use strict';
exports = module.exports = {
initialize,
uninitialize,
getConfig,
getLogs,
initialize: initialize,
uninitialize: uninitialize,
getConfig: getConfig,
getLogs: getLogs,
reboot,
isRebootRequired,
reboot: reboot,
isRebootRequired: isRebootRequired,
onActivated,
onActivated: onActivated,
setupDnsAndCert,
prepareDashboardDomain: prepareDashboardDomain,
setDashboardDomain: setDashboardDomain,
setDashboardAndMailDomain: setDashboardAndMailDomain,
renewCerts: renewCerts,
prepareDashboardDomain,
setDashboardDomain,
updateDashboardDomain,
renewCerts,
setupDashboard: setupDashboard,
runSystemChecks
runSystemChecks: runSystemChecks,
};
const apps = require('./apps.js'),
var addons = require('./addons.js'),
apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
branding = require('./branding.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
@@ -42,12 +42,10 @@ const apps = require('./apps.js'),
platform = require('./platform.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
services = require('./services.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
sysinfo = require('./sysinfo.js'),
tasks = require('./tasks.js'),
users = require('./users.js');
@@ -68,7 +66,7 @@ function uninitialize(callback) {
async.series([
cron.stopJobs,
platform.stopAllTasks
platform.stop
], callback);
}
@@ -80,16 +78,7 @@ function onActivated(callback) {
// 2. the restore code path can run without sudo (since mail/ is non-root)
async.series([
platform.start,
cron.startJobs,
function checkBackupConfiguration(done) {
backups.checkConfiguration(function (error, message) {
if (error) return done(error);
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, done);
});
},
// disable responding to api calls via IP to not leak domain info. this is carefully placed as the last item, so it buys
// the UI some time to query the dashboard domain in the restore code path
(done) => setTimeout(() => reverseProxy.writeDefaultConfig({ activated :true }, done), 30000)
cron.startJobs
], callback);
}
@@ -114,49 +103,24 @@ function notifyUpdate(callback) {
// each of these tasks can fail. we will add some routes to fix/re-run them
function runStartupTasks() {
const tasks = [
// stop all the systemd tasks
platform.stopAllTasks,
// configure nginx to be reachable by IP
reverseProxy.writeDefaultConfig(NOOP_CALLBACK);
// this configures collectd to collect backup storage metrics if filesystem is used. This is also triggerd when the settings change with the rest api
function (callback) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
// this configures collectd to collect backup storage metrics if filesystem is used. This is also triggerd when the settings change with the rest api
settings.getBackupConfig(function (error, backupConfig) {
if (error) return console.error('Failed to read backup config.', error);
backups.configureCollectd(backupConfig, NOOP_CALLBACK);
});
backups.configureCollectd(backupConfig, callback);
});
},
// always generate webadmin config since we have no versioning mechanism for the ejs
if (settings.adminDomain()) reverseProxy.writeAdminConfig(settings.adminDomain(), NOOP_CALLBACK);
// always generate webadmin config since we have no versioning mechanism for the ejs
function (callback) {
if (!settings.adminDomain()) return callback();
// check activation state and start the platform
users.isActivated(function (error, activated) {
if (error) return debug(error);
if (!activated) return debug('initialize: not activated yet'); // not activated
reverseProxy.writeDashboardConfig(settings.adminDomain(), callback);
},
// check activation state and start the platform
function (callback) {
users.isActivated(function (error, activated) {
if (error) return callback(error);
// configure nginx to be reachable by IP when not activated. for the moment, the IP based redirect exists even after domain is setup
// just in case user forgot or some network error happenned in the middle (then browser refresh takes you to activation page)
// we remove the config as a simple security measure to not expose IP <-> domain
if (!activated) {
debug('runStartupTasks: not activated. generating IP based redirection config');
return reverseProxy.writeDefaultConfig({ activated: false }, callback);
}
onActivated(callback);
});
}
];
// we used to run tasks in parallel but simultaneous nginx reloads was causing issues
async.series(async.reflectAll(tasks), function (error, results) {
results.forEach((result, idx) => {
if (result.error) debug(`Startup task at index ${idx} failed: ${result.error.message}`);
});
onActivated(NOOP_CALLBACK);
});
}
@@ -176,17 +140,15 @@ function getConfig(callback) {
version: constants.VERSION,
isDemo: settings.isDemo(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER),
features: appstore.getFeatures(),
profileLocked: allSettings[settings.DIRECTORY_CONFIG_KEY].lockUserProfiles,
mandatory2FA: allSettings[settings.DIRECTORY_CONFIG_KEY].mandatory2FA
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
features: appstore.getFeatures()
});
});
}
function reboot(callback) {
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', '', function (error) {
if (error) debug('reboot: failed to clear reboot notification.', error);
if (error) console.error('Failed to clear reboot notification.', error);
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
});
@@ -204,11 +166,24 @@ function runSystemChecks(callback) {
assert.strictEqual(typeof callback, 'function');
async.parallel([
checkBackupConfiguration,
checkMailStatus,
checkRebootRequired
], callback);
}
function checkBackupConfiguration(callback) {
assert.strictEqual(typeof callback, 'function');
debug('checking backup configuration');
backups.checkConfiguration(function (error, message) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, callback);
});
}
function checkMailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -299,7 +274,7 @@ function prepareDashboardDomain(domain, auditSource, callback) {
const conflict = result.filter(app => app.fqdn === fqdn);
if (conflict.length) return callback(new BoxError(BoxError.BAD_STATE, 'Dashboard location conflicts with an existing app'));
tasks.add(tasks.TASK_SETUP_DNS_AND_CERT, [ constants.ADMIN_LOCATION, domain, auditSource ], function (error, taskId) {
tasks.add(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
@@ -321,12 +296,12 @@ function setDashboardDomain(domain, auditSource, callback) {
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
reverseProxy.writeDashboardConfig(domain, function (error) {
reverseProxy.writeAdminConfig(domain, function (error) {
if (error) return callback(error);
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
settings.setAdminLocation(domain, fqdn, function (error) {
settings.setAdmin(domain, fqdn, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
@@ -338,24 +313,36 @@ function setDashboardDomain(domain, auditSource, callback) {
}
// call this only post activation because it will restart mail server
function updateDashboardDomain(domain, auditSource, callback) {
function setDashboardAndMailDomain(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`updateDashboardDomain: ${domain}`);
debug(`setDashboardAndMailDomain: ${domain}`);
if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
setDashboardDomain(domain, auditSource, function (error) {
if (error) return callback(error);
services.rebuildService('turn', NOOP_CALLBACK); // to update the realm variable
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);
});
}
function setupDashboard(auditSource, progressCallback, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
async.series([
domains.prepareDashboardDomain.bind(null, settings.adminDomain(), auditSource, progressCallback),
setDashboardDomain.bind(null, settings.adminDomain(), auditSource)
], callback);
}
function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
@@ -369,34 +356,3 @@ function renewCerts(options, auditSource, callback) {
callback(null, taskId);
});
}
function setupDnsAndCert(subdomain, domain, auditSource, progressCallback, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
const adminFqdn = domains.fqdn(subdomain, domainObject);
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.series([
(done) => { progressCallback({ message: `Updating DNS of ${adminFqdn}` }); done(); },
domains.upsertDnsRecords.bind(null, subdomain, domain, 'A', [ ip ]),
(done) => { progressCallback({ message: `Waiting for DNS of ${adminFqdn}` }); done(); },
domains.waitForDnsRecord.bind(null, subdomain, domain, 'A', ip, { interval: 30000, times: 50000 }),
(done) => { progressCallback({ message: `Getting certificate of ${adminFqdn}` }); done(); },
reverseProxy.ensureCertificate.bind(null, domains.fqdn(subdomain, domainObject), domain, auditSource)
], function (error) {
if (error) return callback(error);
callback(null);
});
});
});
}
-9
View File
@@ -1,9 +0,0 @@
<Plugin python>
<Module du>
<Path>
Instance "<%= volumeId %>"
Dir "<%= hostPath %>"
</Path>
</Module>
</Plugin>
+4 -11
View File
@@ -26,7 +26,7 @@ exports = module.exports = {
PORT: CLOUDRON ? 3000 : 5454,
INTERNAL_SMTP_PORT: 2525, // this value comes from the mail container
AUTHWALL_PORT: 3001,
SYSADMIN_PORT: 3001, // unused
LDAP_PORT: 3002,
DOCKER_PROXY_PORT: 3003,
@@ -37,14 +37,7 @@ exports = module.exports = {
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js
DEMO_USERNAME: 'cloudron',
DEMO_BLACKLISTED_APPS: [
'com.github.cloudtorrent',
'net.alltubedownload.cloudronapp',
'com.adguard.home.cloudronapp',
'com.transmissionbt.cloudronapp',
'io.github.sickchill.cloudronapp',
'to.couchpota.cloudronapp'
],
DEMO_BLACKLISTED_APPS: [ 'com.github.cloudtorrent' ],
AUTOUPDATE_PATTERN_NEVER: 'never',
@@ -55,8 +48,8 @@ exports = module.exports = {
SUPPORT_EMAIL: 'support@cloudron.io',
FOOTER: '&copy; %YEAR% &nbsp; [Cloudron](https://cloudron.io) &nbsp; &nbsp; &nbsp; [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)',
FOOTER: '&copy; 2020 &nbsp; [Cloudron](https://cloudron.io) &nbsp; &nbsp; &nbsp; [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)',
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '6.0.1-test'
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '5.1.1-test'
};
+70 -27
View File
@@ -4,7 +4,10 @@
// If the patterns overlap all the time, then the task may not ever get a chance to run!
// If you change this change dashboard patterns in settings.html
const DEFAULT_CLEANUP_BACKUPS_PATTERN = '00 30 1,3,5,23 * * *',
DEFAULT_AUTOUPDATE_PATTERN = '00 00 1,3,5,23 * * *';
DEFAULT_BOX_ENSURE_BACKUP_PATTERN_LT_6HOURS = '00 45 1,7,13,19 * * *',
DEFAULT_BOX_ENSURE_BACKUP_PATTERN_GT_6HOURS = '00 45 1,3,5,23 * * *',
DEFAULT_BOX_AUTOUPDATE_PATTERN = '00 00 1,3,5,23 * * *',
DEFAULT_APP_AUTOUPDATE_PATTERN = '00 15 1,3,5,23 * * *';
exports = module.exports = {
startJobs,
@@ -13,11 +16,13 @@ exports = module.exports = {
handleSettingsChanged,
DEFAULT_AUTOUPDATE_PATTERN,
DEFAULT_BOX_AUTOUPDATE_PATTERN,
DEFAULT_APP_AUTOUPDATE_PATTERN
};
var appHealthMonitor = require('./apphealthmonitor.js'),
apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
@@ -36,9 +41,12 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
updateChecker = require('./updatechecker.js');
var gJobs = {
autoUpdater: null,
alive: null, // send periodic stats
appAutoUpdater: null,
boxAutoUpdater: null,
appUpdateChecker: null,
backup: null,
updateChecker: null,
boxUpdateChecker: null,
systemChecks: null,
diskSpaceChecker: null,
certificateRenew: null,
@@ -64,11 +72,15 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function startJobs(callback) {
assert.strictEqual(typeof callback, 'function');
debug('startJobs: starting cron jobs');
const randomMinute = Math.floor(60*Math.random());
gJobs.alive = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // every hour on a random minute
onTick: appstore.sendAliveStatus,
start: true
});
const randomTick = Math.floor(60*Math.random());
gJobs.systemChecks = new CronJob({
cronTime: '00 30 2 * * *', // once a day. if you change this interval, change the notification messages with correct duration
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
start: true
});
@@ -79,10 +91,15 @@ function startJobs(callback) {
start: true
});
// this is run separately from the update itself so that the user can disable automatic updates but can still get a notification
gJobs.updateCheckerJob = new CronJob({
cronTime: `${randomTick} ${randomTick} 1,5,9,13,17,21,23 * * *`,
onTick: () => updateChecker.checkForUpdates({ automatic: true }, NOOP_CALLBACK),
gJobs.boxUpdateCheckerJob = new CronJob({
cronTime: '00 ' + randomMinute + ' 1,3,5,21,23 * * *', // 5 times
onTick: () => updateChecker.checkBoxUpdates({ automatic: true }, NOOP_CALLBACK),
start: true
});
gJobs.appUpdateChecker = new CronJob({
cronTime: '00 ' + randomMinute + ' 2,4,6,20,22 * * *', // 5 times
onTick: () => updateChecker.checkAppUpdates({ automatic: true }, NOOP_CALLBACK),
start: true
});
@@ -133,7 +150,8 @@ function startJobs(callback) {
const tz = allSettings[settings.TIME_ZONE_KEY];
backupConfigChanged(allSettings[settings.BACKUP_CONFIG_KEY], tz);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY], tz);
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY], tz);
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY], tz);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
callback();
@@ -148,7 +166,8 @@ function handleSettingsChanged(key, value) {
switch (key) {
case settings.TIME_ZONE_KEY:
case settings.BACKUP_CONFIG_KEY:
case settings.AUTOUPDATE_PATTERN_KEY:
case settings.APP_AUTOUPDATE_PATTERN_KEY:
case settings.BOX_AUTOUPDATE_PATTERN_KEY:
case settings.DYNAMIC_DNS_KEY:
debug('handleSettingsChanged: recreating all jobs');
async.series([
@@ -165,47 +184,71 @@ function backupConfigChanged(value, tz) {
assert.strictEqual(typeof value, 'object');
assert.strictEqual(typeof tz, 'string');
debug(`backupConfigChanged: schedule ${value.schedulePattern} (${tz})`);
debug(`backupConfigChanged: interval ${value.intervalSecs} (${tz})`);
if (gJobs.backup) gJobs.backup.stop();
let pattern;
if (value.intervalSecs <= 6 * 60 * 60) {
pattern = DEFAULT_BOX_ENSURE_BACKUP_PATTERN_LT_6HOURS; // no option but to backup in the middle of the day
} else {
pattern = DEFAULT_BOX_ENSURE_BACKUP_PATTERN_GT_6HOURS; // avoid middle of the day backups. it's 45 to not overlap auto-updates
}
gJobs.backup = new CronJob({
cronTime: value.schedulePattern,
onTick: backups.startBackupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
cronTime: pattern,
onTick: backups.ensureBackup.bind(null, auditSource.CRON, NOOP_CALLBACK),
start: true,
timeZone: tz
});
}
function autoupdatePatternChanged(pattern, tz) {
function boxAutoupdatePatternChanged(pattern, tz) {
assert.strictEqual(typeof pattern, 'string');
assert.strictEqual(typeof tz, 'string');
debug(`autoupdatePatternChanged: pattern - ${pattern} (${tz})`);
debug(`boxAutoupdatePatternChanged: pattern - ${pattern} (${tz})`);
if (gJobs.autoUpdater) gJobs.autoUpdater.stop();
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.autoUpdater = new CronJob({
gJobs.boxAutoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
const updateInfo = updateChecker.getUpdateInfo();
// do box before app updates. for the off chance that the box logic fixes some app update logic issue
if (updateInfo.box && !updateInfo.box.unstable) {
debug('Starting box autoupdate to %j', updateInfo.box);
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
debug('Starting autoupdate to %j', updateInfo.box);
updater.updateToLatest({ skipBackup: false }, auditSource.CRON, NOOP_CALLBACK);
return;
} else {
debug('No box auto updates available');
}
},
start: true,
timeZone: tz
});
}
if (updateInfo.apps && Object.keys(updateInfo.apps).length > 0) {
function appAutoupdatePatternChanged(pattern, tz) {
assert.strictEqual(typeof pattern, 'string');
assert.strictEqual(typeof tz, 'string');
debug(`appAutoupdatePatternChanged: pattern ${pattern} (${tz})`);
if (gJobs.appAutoUpdater) gJobs.appAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.appAutoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, auditSource.CRON, NOOP_CALLBACK);
} else {
debug('No app auto updates available');
}
},
start: true,
timeZone: tz
});
+1 -1
View File
@@ -144,7 +144,7 @@ function exportToFile(file, callback) {
// latest mysqldump enables column stats by default which is not present in MySQL 5.7 server
// this option must not be set in production cloudrons which still use the old mysqldump
const disableColStats = (constants.TEST && require('fs').readFileSync('/etc/lsb-release', 'utf-8').includes('20.04')) ? '--column-statistics=0' : '';
const disableColStats = (constants.TEST && process.env.DESKTOP_SESSION !== 'ubuntu') ? '--column-statistics=0' : '';
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${disableColStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
+177
View File
@@ -0,0 +1,177 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/caas'),
domains = require('../domains.js'),
settings = require('../settings.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
function formatError(response) {
return util.format('Caas DNS error [%s] %j', response.statusCode, response.body);
}
function getFqdn(location, domain) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
return (location === '') ? domain : location + '-' + domain;
}
function removePrivateFields(domainObject) {
domainObject.config.token = constants.SECRET_PLACEHOLDER;
// do not return the 'key'. in caas, this is private
delete domainObject.fallbackCertificate.key;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
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;
let fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
debug('add: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
var data = {
type: type,
values: values
};
superagent
.post(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.query({ token: dnsConfig.token })
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new BoxError(BoxError.BUSY));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
return callback(null);
});
}
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;
const fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
superagent
.get(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.query({ token: dnsConfig.token, type: type })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
return callback(null, result.body.values);
});
}
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;
debug('del: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
var data = {
type: type,
values: values
};
superagent
.del(settings.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
.query({ token: dnsConfig.token })
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new BoxError(BoxError.BUSY));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
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;
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,
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
};
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);
});
});
}
+4 -7
View File
@@ -119,10 +119,9 @@ function get(domainObject, location, type, callback) {
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) {
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { records }) {
if (error) return callback(error);
const { records } = result;
var tmp = records.map(function (record) { return record.target; });
debug('get: %j', tmp);
@@ -144,10 +143,9 @@ function upsert(domainObject, location, type, values, callback) {
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) {
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
if (error) return callback(error);
const { zoneId, records } = result;
let i = 0, recordIds = []; // used to track available records to update instead of create
async.eachSeries(values, function (value, iteratorCallback) {
@@ -224,10 +222,9 @@ function del(domainObject, location, type, values, callback) {
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) {
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
if (error) return callback(error);
const { zoneId, records } = result;
if (records.length === 0) return callback(null);
var tmp = records.filter(function (record) { return values.some(function (value) { return value === record.target; }); });
@@ -290,7 +287,7 @@ function verifyDnsConfig(domainObject, callback) {
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 linode NS', nameservers);
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' }));
}
+6 -4
View File
@@ -78,11 +78,13 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
return callback(new BoxError(BoxError.EXTERNAL_ERROR, errorMessage));
}
const host = safe.query(tmp, 'CommandResponse[0].DomainDNSGetHostsResult[0].host');
if (!host) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response: ${JSON.stringify(tmp)}`));
if (!Array.isArray(host)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `host is not an array: ${JSON.stringify(tmp)}`));
if (!tmp.CommandResponse[0]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSGetHostsResult[0]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
var hosts = result.ApiResponse.CommandResponse[0].DomainDNSGetHostsResult[0].host.map(function (h) {
return h['$'];
});
const hosts = host.map(h => h['$']);
callback(null, hosts);
});
});
-303
View File
@@ -1,303 +0,0 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/netcup'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
var API_ENDPOINT = 'https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON';
function formatError(response) {
if (response.body) return util.format('Netcup DNS error [%s] %s', response.body.statuscode, response.body.longmessage);
else return util.format('Netcup DNS error [%s] %s', response.statusCode, response.text);
}
function removePrivateFields(domainObject) {
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
// returns a api session id
function login(dnsConfig, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
const data = {
action: 'login',
param:{
apikey: dnsConfig.apiKey,
apipassword: dnsConfig.apiPassword,
customernumber: dnsConfig.customerNumber
}
};
superagent.post(API_ENDPOINT).send(data).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
callback(null, result.body.responsedata.apisessionid);
});
}
function getAllRecords(dnsConfig, apiSessionId, zoneName, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof apiSessionId, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`getAllRecords: getting dns records of ${zoneName}`);
const data = {
action: 'infoDnsRecords',
param:{
apikey: dnsConfig.apiKey,
apisessionid: apiSessionId,
customernumber: dnsConfig.customerNumber,
domainname: zoneName,
}
};
superagent.post(API_ENDPOINT).send(data).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
debug('getAllRecords:', JSON.stringify(result.body.responsedata.dnsrecords || []));
callback(null, result.body.responsedata.dnsrecords || []);
});
}
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);
login(dnsConfig, function (error, apiSessionId) {
if (error) return callback(error);
getAllRecords(dnsConfig, apiSessionId, zoneName, function (error, result) {
if (error) return callback(error);
let records = [];
values.forEach(function (value) {
// remove possible quotation
if (value.charAt(0) === '"') value = value.slice(1);
if (value.charAt(value.length -1) === '"') value = value.slice(0, -1);
let priority = null;
if (type === 'MX') {
priority = parseInt(value.split(' ')[0], 10);
value = value.split(' ')[1];
}
let record = result.find(function (r) { return r.hostname === name && r.type === type; });
if (!record) record = { hostname: name, type: type, destination: value, deleterecord: false };
else record.destination = value;
if (priority !== null) record.priority = priority;
records.push(record);
});
const data = {
action: 'updateDnsRecords',
param:{
apikey: dnsConfig.apiKey,
apisessionid: apiSessionId,
customernumber: dnsConfig.customerNumber,
domainname: zoneName,
dnsrecordset: {
dnsrecords: records
}
}
};
debug('upserting', JSON.stringify(data));
superagent.post(API_ENDPOINT).send(data).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
if (result.body.statuscode !== 2000) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
debug('upsert:', result.body);
callback(null);
});
});
});
}
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) || '@';
debug('get: %s for zone %s of type %s', name, zoneName, type);
login(dnsConfig, function (error, apiSessionId) {
if (error) return callback(error);
getAllRecords(dnsConfig, apiSessionId, zoneName, function (error, result) {
if (error) return callback(error);
// We only return the value string
callback(null, result.filter(function (r) { return r.hostname === name && r.type === type; }).map(function (r) { return r.destination; }));
});
});
}
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) || '@';
debug('del: %s for zone %s of type %s with values %j', name, zoneName, type, values);
login(dnsConfig, function (error, apiSessionId) {
if (error) return callback(error);
getAllRecords(dnsConfig, apiSessionId, zoneName, function (error, result) {
if (error) return callback(error);
let records = [];
values.forEach(function (value) {
// remove possible quotation
if (value.charAt(0) === '"') value = value.slice(1);
if (value.charAt(value.length -1) === '"') value = value.slice(0, -1);
let record = result.find(function (r) { return r.hostname === name && r.type === type && r.destination === value; });
if (!record) return;
record.deleterecord = true;
records.push(record);
});
if (records.length === 0) return callback(null);
const data = {
action: 'updateDnsRecords',
param:{
apikey: dnsConfig.apiKey,
apisessionid: apiSessionId,
customernumber: dnsConfig.customerNumber,
domainname: zoneName,
dnsrecordset: {
dnsrecords: records
}
}
};
superagent.post(API_ENDPOINT).send(data).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
if (result.body.statuscode !== 2000) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
debug('del:', result.body.responsedata);
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.customerNumber || typeof dnsConfig.customerNumber !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'customerNumber must be a non-empty string', { field: 'customerNumber' }));
if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'apiKey must be a non-empty string', { field: 'apiKey' }));
if (!dnsConfig.apiPassword || typeof dnsConfig.apiPassword !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'apiPassword must be a non-empty string', { field: 'apiPassword' }));
const ip = '127.0.0.1';
var credentials = {
customerNumber: dnsConfig.customerNumber,
apiKey: dnsConfig.apiKey,
apiPassword: dnsConfig.apiPassword,
};
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.every(function (n) { return n.toLowerCase().indexOf('dns.netcup.net') !== -1; })) {
debug('verifyDnsConfig: %j does not contains Netcup NS', nameservers);
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Netcup', { 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);
});
});
});
}
+2 -3
View File
@@ -281,14 +281,13 @@ function verifyDnsConfig(domainObject, callback) {
}
const location = 'cloudrontestdns';
const newDomainObject = Object.assign({ }, domainObject, { config: credentials });
upsert(newDomainObject, location, 'A', [ ip ], function (error) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
del(newDomainObject, location, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+120 -195
View File
@@ -1,39 +1,38 @@
'use strict';
exports = module.exports = {
testRegistryConfig,
setRegistryConfig,
injectPrivateFields,
removePrivateFields,
testRegistryConfig: testRegistryConfig,
setRegistryConfig: setRegistryConfig,
injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields,
ping,
ping: ping,
info,
downloadImage,
createContainer,
startContainer,
restartContainer,
stopContainer,
info: info,
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
restartContainer: restartContainer,
stopContainer: stopContainer,
stopContainerByName: stopContainer,
stopContainers,
deleteContainer,
deleteImage,
deleteContainers,
createSubcontainer,
getContainerIdByIp,
inspect,
getContainerIp,
stopContainers: stopContainers,
deleteContainer: deleteContainer,
deleteContainerByName: deleteContainer,
deleteImage: deleteImage,
deleteContainers: deleteContainers,
createSubcontainer: createSubcontainer,
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
execContainer,
getEvents,
memoryUsage,
createVolume,
removeVolume,
clearVolume,
update
execContainer: execContainer,
getEvents: getEvents,
memoryUsage: memoryUsage,
createVolume: createVolume,
removeVolume: removeVolume,
clearVolume: clearVolume
};
const apps = require('./apps.js'),
var addons = require('./addons.js'),
async = require('async'),
assert = require('assert'),
BoxError = require('./boxerror.js'),
@@ -41,15 +40,11 @@ const apps = require('./apps.js'),
constants = require('./constants.js'),
debug = require('debug')('box:docker'),
Docker = require('dockerode'),
os = require('os'),
path = require('path'),
services = require('./services.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
safe = require('safetydance'),
system = require('./system.js'),
util = require('util'),
volumes = require('./volumes.js'),
_ = require('underscore');
const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
@@ -176,53 +171,26 @@ function downloadImage(manifest, callback) {
debug('downloadImage %s', manifest.dockerImage);
const image = gConnection.getImage(manifest.dockerImage);
var attempt = 1;
image.inspect(function (error, result) {
if (!error && result) return callback(null); // image is already present locally
async.retry({ times: 10, interval: 5000, errorFilter: e => e.reason !== BoxError.NOT_FOUND }, function (retryCallback) {
debug('Downloading image %s. attempt: %s', manifest.dockerImage, attempt++);
let attempt = 1;
async.retry({ times: 10, interval: 5000, errorFilter: e => e.reason !== BoxError.NOT_FOUND }, function (retryCallback) {
debug('Downloading image %s. attempt: %s', manifest.dockerImage, attempt++);
pullImage(manifest, retryCallback);
}, callback);
});
pullImage(manifest, retryCallback);
}, callback);
}
function getBinds(app, callback) {
function getBindsSync(app) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.mounts.length === 0) return callback(null);
let binds = [];
volumes.list(function (error, result) {
if (error) return callback(error);
let volumesById = {};
result.forEach(r => volumesById[r.id] = r);
for (const mount of app.mounts) {
const volume = volumesById[mount.volumeId];
binds.push(`${volume.hostPath}:/media/${volume.name}:${mount.readOnly ? 'ro' : 'rw'}`);
}
callback(null, binds);
});
}
function getLowerUpIp() { // see getifaddrs and IFF_LOWER_UP and netdevice
const ni = os.networkInterfaces(); // { lo: [], eth0: [] }
for (const iname of Object.keys(ni)) {
if (iname === 'lo') continue;
for (const address of ni[iname]) {
if (!address.internal && address.family === 'IPv4') return address.address;
}
for (let name of Object.keys(app.binds)) {
const bind = app.binds[name];
binds.push(`${bind.hostPath}:/media/${name}:${bind.readOnly ? 'ro' : 'rw'}`);
}
return null;
return binds;
}
function createSubcontainer(app, name, cmd, options, callback) {
@@ -232,11 +200,12 @@ function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let isAppContainer = !cmd; // non app-containers are like scheduler
let isAppContainer = !cmd; // non app-containers are like scheduler and exec (terminal) containers
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
const hostname = isAppContainer ? app.id : name;
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
@@ -250,6 +219,11 @@ function createSubcontainer(app, name, cmd, options, callback) {
`${envPrefix}APP_DOMAIN=${domain}`
];
// docker portBindings requires ports to be exposed
exposedPorts[manifest.httpPort + '/tcp'] = {};
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
var portEnv = [];
for (let portName in app.portBindings) {
const hostPort = app.portBindings[portName];
@@ -258,121 +232,102 @@ function createSubcontainer(app, name, cmd, options, callback) {
var containerPort = ports[portName].containerPort || hostPort;
// docker portBindings requires ports to be exposed
exposedPorts[`${containerPort}/${portType}`] = {};
portEnv.push(`${portName}=${hostPort}`);
const hostIp = hostPort === 53 ? getLowerUpIp() : '0.0.0.0'; // port 53 is special because it is possibly taken by systemd-resolved
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: hostIp, HostPort: hostPort + '' } ];
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
}
let appEnv = [];
Object.keys(app.env).forEach(function (name) { appEnv.push(`${name}=${app.env[name]}`); });
let memoryLimit = apps.getMemoryLimit(app);
// first check db record, then manifest
var memoryLimit = app.memoryLimit || manifest.memoryLimit || 0;
if (memoryLimit === -1) { // unrestricted
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit < constants.DEFAULT_MEMORY_LIMIT) { // ensure we never go below minimum (in case we change the default)
memoryLimit = constants.DEFAULT_MEMORY_LIMIT;
}
// give scheduler tasks twice the memory limit since background jobs take more memory
// if required, we can make this a manifest and runtime argument later
if (!isAppContainer) memoryLimit *= 2;
services.getEnvironment(app, function (error, addonEnv) {
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(error);
getBinds(app, function (error, binds) {
if (error) return callback(error);
let containerOptions = {
name: name, // for referencing containers
Tty: isAppContainer,
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
ExposedPorts: isAppContainer ? exposedPorts : { },
Volumes: { // see also ReadonlyRootfs
'/tmp': {},
'/run': {}
},
Labels: {
'fqdn': app.fqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer),
'isCloudronManaged': String(true)
},
HostConfig: {
Mounts: services.getMountsSync(app, app.manifest.addons),
Binds: binds, // ideally, we have to use 'Mounts' but we have to create volumes then
LogConfig: {
Type: 'syslog',
Config: {
'tag': app.id,
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
'syslog-format': 'rfc5424'
}
},
Memory: system.getMemoryAllocation(memoryLimit),
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: isAppContainer ? dockerPortBindings : { },
PublishAllPorts: false,
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
RestartPolicy: {
'Name': isAppContainer ? 'unless-stopped' : 'no',
'MaximumRetryCount': 0
},
CpuShares: app.cpuShares,
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapAdd: [],
CapDrop: []
}
};
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
// name to look up the internal docker ip. this makes curl from within container fail
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
// Hostname cannot be set with container NetworkMode. Subcontainers run is the network space of the app container
// This is done to prevent lots of up/down events and iptables locking
if (isAppContainer) {
containerOptions.Hostname = app.id;
containerOptions.HostConfig.NetworkMode = 'cloudron'; // user defined bridge network
containerOptions.HostConfig.Dns = ['172.18.0.1']; // use internal dns
containerOptions.HostConfig.DnsSearch = ['.']; // use internal dns
containerOptions.NetworkingConfig = {
EndpointsConfig: {
cloudron: {
IPAMConfig: {
IPv4Address: app.containerIp
},
Aliases: [ name ] // adds hostname entry with container name
}
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
// name to look up the internal docker ip. this makes curl from within container fail
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
// Hostname cannot be set with container NetworkMode
var containerOptions = {
name: name, // for referencing containers
Tty: isAppContainer,
Hostname: hostname,
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
ExposedPorts: isAppContainer ? exposedPorts : { },
Volumes: { // see also ReadonlyRootfs
'/tmp': {},
'/run': {}
},
Labels: {
'fqdn': app.fqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer),
'isCloudronManaged': String(true)
},
HostConfig: {
Mounts: addons.getMountsSync(app, app.manifest.addons),
Binds: getBindsSync(app), // ideally, we have to use 'Mounts' but we have to create volumes then
LogConfig: {
Type: 'syslog',
Config: {
'tag': app.id,
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
'syslog-format': 'rfc5424'
}
};
} else {
containerOptions.HostConfig.NetworkMode = `container:${app.containerId}`; // scheduler containers must have same IP as app for various addon auth
},
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: isAppContainer ? dockerPortBindings : { },
PublishAllPorts: false,
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
RestartPolicy: {
'Name': isAppContainer ? 'unless-stopped' : 'no',
'MaximumRetryCount': 0
},
CpuShares: app.cpuShares,
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
NetworkMode: 'cloudron', // user defined bridge network
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapAdd: [],
CapDrop: [ 'NET_RAW' ] // https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
},
NetworkingConfig: {
EndpointsConfig: {
cloudron: {
Aliases: [ name ] // this allows sub-containers reach app containers by name
}
}
}
};
var capabilities = manifest.capabilities || [];
var capabilities = manifest.capabilities || [];
if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping
if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker
containerOptions = _.extend(containerOptions, options);
if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) {
containerOptions.HostConfig.Devices = [
{ PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' }
];
}
gConnection.createContainer(containerOptions, function (error, container) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
containerOptions = _.extend(containerOptions, options);
gConnection.createContainer(containerOptions, function (error, container) {
if (error && error.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback(null, container);
});
callback(null, container);
});
});
}
@@ -437,7 +392,7 @@ function stopContainer(containerId, callback) {
});
}
function deleteContainer(containerId, callback) { // id can also be name
function deleteContainer(containerId, callback) {
assert(!containerId || typeof containerId === 'string');
assert.strictEqual(typeof callback, 'function');
@@ -549,29 +504,13 @@ function inspect(containerId, callback) {
var container = gConnection.getContainer(containerId);
container.inspect(function (error, result) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, `Unable to find container ${containerId}`));
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback(null, result);
});
}
function getContainerIp(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
if (constants.TEST) return callback(null, '127.0.5.5');
inspect(containerId, function (error, result) {
if (error) return callback(error);
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error getting container IP'));
callback(null, ip);
});
}
function execContainer(containerId, options, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof options, 'object');
@@ -694,17 +633,3 @@ function info(callback) {
callback(null, result);
});
}
function update(name, memory, memorySwap, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof memory, 'number');
assert.strictEqual(typeof memorySwap, 'number');
assert.strictEqual(typeof callback, 'function');
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${name}`.split(' ');
// scale back db containers, if possible. this is retried because updating memory constraints can fail
// with failed to write to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/xx/memory.memsw.limit_in_bytes: device or resource busy
async.retry({ times: 10, interval: 60 * 1000 }, function (retryCallback) {
shell.spawn(`update(${name})`, '/usr/bin/docker', args, { }, retryCallback);
}, callback);
}
+1 -1
View File
@@ -55,7 +55,7 @@ function attachDockerRequest(req, res, next) {
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
res.write(' ');
dockerResponse.on('error', function (error) { debug('dockerResponse error:', error); });
dockerResponse.on('error', function (error) { console.error('dockerResponse error:', error); });
dockerResponse.pipe(res, { end: true });
});
+2 -9
View File
@@ -16,18 +16,14 @@ var assert = require('assert'),
database = require('./database.js'),
safe = require('safetydance');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'wellKnownJson' ].join(',');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson' ].join(',');
function postProcess(data) {
data.config = safe.JSON.parse(data.configJson);
delete data.configJson;
data.tlsConfig = safe.JSON.parse(data.tlsConfigJson);
delete data.configJson;
delete data.tlsConfigJson;
data.wellKnown = safe.JSON.parse(data.wellKnownJson);
delete data.wellKnownJson;
return data;
}
@@ -90,9 +86,6 @@ function update(name, domain, callback) {
} else if (k === 'tlsConfig') {
fields.push('tlsConfigJson = ?');
args.push(JSON.stringify(domain[k]));
} else if (k === 'wellKnown') {
fields.push('wellKnownJson = ?');
args.push(JSON.stringify(domain[k]));
} else {
fields.push(k + ' = ?');
args.push(domain[k]);
+85 -44
View File
@@ -1,35 +1,38 @@
'use strict';
module.exports = exports = {
add,
get,
getAll,
update,
del,
clear,
add: add,
get: get,
getAll: getAll,
update: update,
del: del,
clear: clear,
fqdn,
getName,
fqdn: fqdn,
getName: getName,
getDnsRecords,
upsertDnsRecords,
removeDnsRecords,
getDnsRecords: getDnsRecords,
upsertDnsRecords: upsertDnsRecords,
removeDnsRecords: removeDnsRecords,
waitForDnsRecord,
waitForDnsRecord: waitForDnsRecord,
removePrivateFields,
removeRestrictedFields,
removePrivateFields: removePrivateFields,
removeRestrictedFields: removeRestrictedFields,
validateHostname,
validateHostname: validateHostname,
makeWildcard,
makeWildcard: makeWildcard,
parentDomain,
parentDomain: parentDomain,
checkDnsRecords
checkDnsRecords: checkDnsRecords,
prepareDashboardDomain: prepareDashboardDomain
};
var assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:domains'),
@@ -51,6 +54,7 @@ function api(provider) {
assert.strictEqual(typeof provider, 'string');
switch (provider) {
case 'caas': return require('./dns/caas.js');
case 'cloudflare': return require('./dns/cloudflare.js');
case 'route53': return require('./dns/route53.js');
case 'gcdns': return require('./dns/gcdns.js');
@@ -60,7 +64,6 @@ function api(provider) {
case 'linode': return require('./dns/linode.js');
case 'namecom': return require('./dns/namecom.js');
case 'namecheap': return require('./dns/namecheap.js');
case 'netcup': return require('./dns/netcup.js');
case 'noop': return require('./dns/noop.js');
case 'manual': return require('./dns/manual.js');
case 'wildcard': return require('./dns/wildcard.js');
@@ -90,12 +93,14 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) {
if (error && error.reason === BoxError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.BAD_FIELD, `Configuration error: ${error.message}`));
if (error) return callback(error);
result.hyphenatedSubdomains = !!dnsConfig.hyphenatedSubdomains;
callback(null, result);
});
}
function fqdn(location, domainObject) {
return location + (location ? '.' : '') + domainObject.domain;
return location + (location ? (domainObject.config.hyphenatedSubdomains ? '-' : '.') : '') + domainObject.domain;
}
// Hostname validation comes from RFC 1123 (section 2.1)
@@ -129,6 +134,10 @@ function validateHostname(location, domainObject) {
if (/^[-.]/.test(location)) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot', { field: 'location' });
}
if (domainObject.config.hyphenatedSubdomains) {
if (location.indexOf('.') !== -1) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot contain a dot', { field: 'location' });
}
return null;
}
@@ -140,9 +149,10 @@ function validateTlsConfig(tlsConfig, dnsProvider) {
case 'letsencrypt-prod':
case 'letsencrypt-staging':
case 'fallback':
case 'caas':
break;
default:
return new BoxError(BoxError.BAD_FIELD, 'tlsConfig.provider must be fallback, letsencrypt-prod/staging', { field: 'tlsProvider' });
return new BoxError(BoxError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback, letsencrypt-prod/staging', { field: 'tlsProvider' });
}
if (tlsConfig.wildcard) {
@@ -153,12 +163,6 @@ function validateTlsConfig(tlsConfig, dnsProvider) {
return null;
}
function validateWellKnown(wellKnown) {
assert.strictEqual(typeof wellKnown, 'object');
return null;
}
function add(domain, data, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data.zoneName, 'string');
@@ -185,7 +189,7 @@ function add(domain, data, auditSource, callback) {
if (error) return callback(error);
} else {
fallbackCertificate = reverseProxy.generateFallbackCertificateSync({ domain, config });
if (fallbackCertificate.error) return callback(fallbackCertificate.error);
if (fallbackCertificate.error) return callback(error);
}
let error = validateTlsConfig(tlsConfig, provider);
@@ -253,7 +257,7 @@ function update(domain, data, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
let { zoneName, provider, config, fallbackCertificate, tlsConfig, wellKnown } = data;
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
if (settings.isDemo() && (domain === settings.adminDomain())) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
@@ -274,9 +278,6 @@ function update(domain, data, auditSource, callback) {
error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
error = validateWellKnown(wellKnown, provider);
if (error) return callback(error);
if (provider === domainObject.provider) api(provider).injectPrivateFields(config, domainObject.config);
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
@@ -284,10 +285,9 @@ function update(domain, data, auditSource, callback) {
let newData = {
config: sanitizedConfig,
zoneName,
provider,
tlsConfig,
wellKnown
zoneName: zoneName,
provider: provider,
tlsConfig: tlsConfig
};
domaindb.update(domain, newData, function (error) {
@@ -313,7 +313,6 @@ function del(domain, auditSource, callback) {
assert.strictEqual(typeof callback, 'function');
if (domain === settings.adminDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove admin domain'));
if (domain === settings.mailDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove mail domain'));
domaindb.del(domain, function (error) {
if (error) return callback(error);
@@ -342,7 +341,19 @@ function getName(domain, location, type) {
if (location === '') return part;
return part ? `${location}.${part}` : location;
if (!domain.config.hyphenatedSubdomains) return part ? `${location}.${part}` : location;
// hyphenatedSubdomains
if (type !== 'TXT') return `${location}-${part}`;
if (location.startsWith('_acme-challenge.')) {
return `${location}-${part}`;
} else if (location === '_acme-challenge') {
const up = part.replace(/^[^.]*\.?/, ''); // this gets the domain one level up
return up ? `${location}.${up}` : location;
} else {
return `${location}.${part}`;
}
}
function getDnsRecords(location, domain, type, callback) {
@@ -442,7 +453,7 @@ function waitForDnsRecord(location, domain, type, value, options, callback) {
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'wellKnown');
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate');
return api(result.provider).removePrivateFields(result);
}
@@ -450,16 +461,46 @@ function removePrivateFields(domain) {
function removeRestrictedFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider');
result.config = {}; // always ensure config object
// always ensure config object
result.config = { hyphenatedSubdomains: !!domain.config.hyphenatedSubdomains };
return result;
}
function makeWildcard(vhost) {
assert.strictEqual(typeof vhost, 'string');
function makeWildcard(hostname) {
assert.strictEqual(typeof hostname, 'string');
// if the vhost is like *.example.com, this function will do nothing
let parts = vhost.split('.');
let parts = hostname.split('.');
parts[0] = '*';
return parts.join('.');
}
function prepareDashboardDomain(domain, auditSource, progressCallback, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, domainObject) {
if (error) return callback(error);
const adminFqdn = fqdn(constants.ADMIN_LOCATION, domainObject);
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.series([
(done) => { progressCallback({ percent: 10, message: `Updating DNS of ${adminFqdn}` }); done(); },
upsertDnsRecords.bind(null, constants.ADMIN_LOCATION, domain, 'A', [ ip ]),
(done) => { progressCallback({ percent: 40, message: `Waiting for DNS of ${adminFqdn}` }); done(); },
waitForDnsRecord.bind(null, constants.ADMIN_LOCATION, domain, 'A', ip, { interval: 30000, times: 50000 }),
(done) => { progressCallback({ percent: 70, message: `Getting certificate of ${adminFqdn}` }); done(); },
reverseProxy.ensureCertificate.bind(null, fqdn(constants.ADMIN_LOCATION, domainObject), domain, auditSource)
], function (error) {
if (error) return callback(error);
callback(null);
});
});
});
}
+8 -30
View File
@@ -1,12 +1,11 @@
'use strict';
exports = module.exports = {
add,
upsert,
get,
getAllPaged,
getByCreationTime,
cleanup,
add: add,
get: get,
getAllPaged: getAllPaged,
getByCreationTime: getByCreationTime,
cleanup: cleanup,
// keep in sync with webadmin index.js filter
ACTION_ACTIVATE: 'cloudron.activate',
@@ -40,7 +39,6 @@ exports = module.exports = {
ACTION_DOMAIN_UPDATE: 'domain.update',
ACTION_DOMAIN_REMOVE: 'domain.remove',
ACTION_MAIL_LOCATION: 'mail.location',
ACTION_MAIL_ENABLED: 'mail.enabled',
ACTION_MAIL_DISABLED: 'mail.disabled',
ACTION_MAIL_MAILBOX_ADD: 'mail.box.add',
@@ -58,15 +56,10 @@ exports = module.exports = {
ACTION_USER_ADD: 'user.add',
ACTION_USER_LOGIN: 'user.login',
ACTION_USER_LOGOUT: 'user.logout',
ACTION_USER_REMOVE: 'user.remove',
ACTION_USER_UPDATE: 'user.update',
ACTION_USER_TRANSFER: 'user.transfer',
ACTION_VOLUME_ADD: 'volume.add',
ACTION_VOLUME_UPDATE: 'volume.update',
ACTION_VOLUME_REMOVE: 'volume.remove',
ACTION_DYNDNS_UPDATE: 'dyndns.update',
ACTION_SUPPORT_TICKET: 'support.ticket',
@@ -92,24 +85,9 @@ function add(action, source, data, callback) {
callback = callback || NOOP_CALLBACK;
eventlogdb.add(uuid.v4(), action, source, data, function (error, id) {
if (error) return callback(error);
callback(null, { id: id });
notifications.onEvent(id, action, source, data, NOOP_CALLBACK);
});
}
function upsert(action, source, data, callback) {
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
assert(!callback || typeof callback === 'function');
callback = callback || NOOP_CALLBACK;
eventlogdb.upsert(uuid.v4(), action, source, data, function (error, id) {
// we do only daily upserts for login actions, so they don't spam the db
var api = action === exports.ACTION_USER_LOGIN ? eventlogdb.upsert : eventlogdb.add;
api(uuid.v4(), action, source, data, function (error, id) {
if (error) return callback(error);
callback(null, { id: id });
+24 -35
View File
@@ -42,14 +42,14 @@ function translateUser(ldapConfig, ldapUser) {
return {
username: ldapUser[ldapConfig.usernameField],
email: ldapUser.mail || ldapUser.mailPrimaryAddress,
email: ldapUser.mail,
displayName: ldapUser.cn // user.giveName + ' ' + user.sn
};
}
function validUserRequirements(user) {
if (!user.username || !user.email || !user.displayName) {
debug(`[Invalid LDAP user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
debug(`[LDAP user empty username/email/displayName] username=${user.username} email=${user.email} displayName=${user.displayName}`);
return false;
} else {
return true;
@@ -57,9 +57,8 @@ function validUserRequirements(user) {
}
// performs service bind if required
function getClient(externalLdapConfig, doBindAuth, callback) {
function getClient(externalLdapConfig, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof doBindAuth, 'boolean');
assert.strictEqual(typeof callback, 'function');
// ensure we only callback once since we also have to listen to client.error events
@@ -89,14 +88,13 @@ function getClient(externalLdapConfig, doBindAuth, callback) {
callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
});
// skip bind auth if none exist or if not wanted
if (!externalLdapConfig.bindDn || !doBindAuth) return callback(null, client);
if (!externalLdapConfig.bindDn) return callback(null, client);
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
callback(null, client);
callback(null, client, externalLdapConfig);
});
}
@@ -105,7 +103,7 @@ function ldapGetByDN(externalLdapConfig, dn, callback) {
assert.strictEqual(typeof dn, 'string');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, true, function (error, client) {
getClient(externalLdapConfig, function (error, client) {
if (error) return callback(error);
let searchOptions = {
@@ -115,9 +113,6 @@ function ldapGetByDN(externalLdapConfig, dn, callback) {
debug(`Get object at ${dn}`);
// basic validation to not crash
try { ldap.parseDN(dn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid DN')); }
client.search(dn, searchOptions, function (error, result) {
if (error instanceof ldap.NoSuchObjectError) return callback(new BoxError(BoxError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
@@ -145,7 +140,7 @@ function ldapUserSearch(externalLdapConfig, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, true, function (error, client) {
getClient(externalLdapConfig, function (error, client) {
if (error) return callback(error);
let searchOptions = {
@@ -186,7 +181,7 @@ function ldapGroupSearch(externalLdapConfig, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, true, function (error, client) {
getClient(externalLdapConfig, function (error, client) {
if (error) return callback(error);
let searchOptions = {
@@ -253,7 +248,7 @@ function testConfig(config, callback) {
if (!config.groupnameField || typeof config.groupnameField !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
}
getClient(config, true, function (error, client) {
getClient(config, function (error, client) {
if (error) return callback(error);
var opts = {
@@ -310,7 +305,7 @@ function createAndVerifyUserIfNotExist(identifier, password, callback) {
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE, function (error, user) {
if (error) {
debug(`createAndVerifyUserIfNotExist: Failed to auto create user ${user.username}`, error);
console.error('Failed to auto create user', user.username, error);
return callback(new BoxError(BoxError.INTERNAL_ERROR));
}
@@ -337,15 +332,12 @@ function verifyPassword(user, password, callback) {
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
getClient(externalLdapConfig, false, function (error, client) {
if (error) return callback(error);
let client = ldap.createClient({ url: externalLdapConfig.url });
client.bind(ldapUsers[0].dn, password, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
client.bind(ldapUsers[0].dn, password, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
});
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
});
});
});
@@ -399,7 +391,7 @@ function syncUsers(externalLdapConfig, progressCallback, callback) {
debug(`[adding user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
if (error) debug('syncUsers: Failed to create user', user, error.message);
if (error) console.error('Failed to create user', user, error.message);
iteratorCallback();
});
} else if (result.source !== 'ldap') {
@@ -464,7 +456,7 @@ function syncGroups(externalLdapConfig, progressCallback, callback) {
debug(`[adding group] groupname=${groupName}`);
groups.create(groupName, 'ldap', function (error) {
if (error) debug('syncGroups: Failed to create group', groupName, error);
if (error) console.error('Failed to create group', groupName, error);
iteratorCallback();
});
} else {
@@ -506,7 +498,7 @@ function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
ldapGroupSearch(externalLdapConfig, {}, function (error, result) {
if (error) return callback(error);
if (!result || result.length === 0) {
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
console.error(`Unable to find group ${group.name} ignoring for now.`);
return callback();
}
@@ -517,21 +509,18 @@ function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
});
if (!found) {
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
console.error(`Unable to find group ${group.name} ignoring for now.`);
return callback();
}
var ldapGroupMembers = found.member || found.uniqueMember || [];
// if only one entry is in the group ldap returns a string, not an array!
if (typeof ldapGroupMembers === 'string') ldapGroupMembers = [ ldapGroupMembers ];
var ldapGroupMembers = found.member || [];
debug(`Group ${group.name} has ${ldapGroupMembers.length} members.`);
async.eachSeries(ldapGroupMembers, function (memberDn, iteratorCallback) {
ldapGetByDN(externalLdapConfig, memberDn, function (error, result) {
if (error) {
debug(`Failed to get ${memberDn}:`, error);
console.error(`Failed to get ${memberDn}:`, error);
return iteratorCallback();
}
@@ -542,18 +531,18 @@ function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
users.getByUsername(username, function (error, result) {
if (error) {
debug(`syncGroupUsers: Failed to get user by username ${username}`, error);
console.error(`Failed to get user by username ${username}`, error);
return iteratorCallback();
}
groups.addMember(group.id, result.id, function (error) {
if (error && error.reason !== BoxError.ALREADY_EXISTS) debug('syncGroupUsers: Failed to add member', error);
if (error && error.reason !== BoxError.ALREADY_EXISTS) console.error('Failed to add member', error);
iteratorCallback();
});
});
});
}, function (error) {
if (error) debug('syncGroupUsers: ', error);
if (error) console.error(error);
iteratorCallback();
});
});
+8 -17
View File
@@ -1,27 +1,22 @@
'use strict';
exports = module.exports = {
start,
DEFAULT_MEMORY_LIMIT: 256 * 1024 * 1024
startGraphite: startGraphite
};
var assert = require('assert'),
async = require('async'),
infra = require('./infra_version.js'),
paths = require('./paths.js'),
shell = require('./shell.js'),
system = require('./system.js');
shell = require('./shell.js');
function start(existingInfra, serviceConfig, callback) {
function startGraphite(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof serviceConfig, 'object');
assert.strictEqual(typeof callback, 'function');
const tag = infra.images.graphite.tag;
const dataDir = paths.PLATFORM_DATA_DIR;
const memoryLimit = serviceConfig.memoryLimit || exports.DEFAULT_MEMORY_LIMIT;
const memory = system.getMemoryAllocation(memoryLimit);
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
const cmd = `docker run --restart=always -d --name="graphite" \
--hostname graphite \
@@ -31,8 +26,8 @@ function start(existingInfra, serviceConfig, callback) {
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=graphite \
-m ${memory} \
--memory-swap ${memoryLimit} \
-m 150m \
--memory-swap 150m \
--dns 172.18.0.1 \
--dns-search=. \
-p 127.0.0.1:2003:2003 \
@@ -42,9 +37,5 @@ function start(existingInfra, serviceConfig, callback) {
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
async.series([
shell.exec.bind(null, 'stopGraphite', 'docker stop graphite || true'),
shell.exec.bind(null, 'removeGraphite', 'docker rm -f graphite || true'),
shell.exec.bind(null, 'startGraphite', cmd)
], callback);
shell.exec('startGraphite', cmd, callback);
}
+2 -3
View File
@@ -84,8 +84,9 @@ function getAll(callback) {
function getAllWithMembers(callback) {
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' GROUP BY userGroups.id ORDER BY name', function (error, results) {
' GROUP BY userGroups.id', function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
@@ -196,7 +197,6 @@ function setMembers(groupId, userIds, callback) {
database.transaction(queries, function (error) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.CONFLICT, 'Duplicate member in list'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(error);
@@ -228,7 +228,6 @@ function setMembership(userId, groupIds, callback) {
database.transaction(queries, function (error) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, error.message));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.CONFLICT, 'Already member'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
+16 -16
View File
@@ -1,25 +1,25 @@
'use strict';
exports = module.exports = {
create,
remove,
get,
getByName,
update,
getWithMembers,
getAll,
getAllWithMembers,
create: create,
remove: remove,
get: get,
getByName: getByName,
update: update,
getWithMembers: getWithMembers,
getAll: getAll,
getAllWithMembers: getAllWithMembers,
getMembers,
addMember,
setMembers,
removeMember,
isMember,
getMembers: getMembers,
addMember: addMember,
setMembers: setMembers,
removeMember: removeMember,
isMember: isMember,
setMembership,
getMembership,
setMembership: setMembership,
getMembership: getMembership,
count
count: count
};
var assert = require('assert'),
+7 -7
View File
@@ -6,7 +6,7 @@
exports = module.exports = {
// a version change recreates all containers with latest docker config
'version': '48.18.0',
'version': '48.17.0',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:2.0.0@sha256:f9fea80513aa7c92fe2e7bf3978b54c8ac5222f47a9a32a7f8833edf0eb5a4f4' }
@@ -15,13 +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': {
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.2.0@sha256:4359aae80050a92bae3be30600fb93ef4dbaec6dc9254bda353c0b131a36f969' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.2@sha256:dd624870c7f8ba9b2759f93ce740d1e092a1ac4b2d6af5007a01b30ad6b316d0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:3.3.0@sha256:0daf1be5320c095077392bf21d247b93ceaddca46c866c17259a335c80d2f357' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:3.0.0@sha256:59e50b1f55e433ffdf6d678f8c658812b4119f631db8325572a52ee40d3bc562' },
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.1.0@sha256:e1dd22aa6eef5beb7339834b200a8bb787ffc2264ce11139857a054108fefb4f' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.1@sha256:c1145d43c8a912fe6f5a5629a4052454a4aa6f23391c1efbffeec9d12d72a256' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.2.1@sha256:430f3e8b700327d4afa03a7b4e10a8b5544f171e0946ead8cdc5b67ee32db8e4' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.2.0@sha256:205486ff0f6bf6854610572df401cf3651bc62baf28fd26e9c5632497f10c2cb' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.3.0@sha256:0e31ec817e235b1814c04af97b1e7cf0053384aca2569570ce92bef0d95e94d2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.1.0@sha256:18e0d75ad88a3e66849de2c4c01f794e8df9235befd74544838e34b65f487740' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.9.4@sha256:0e169b97a0584a76197d2bbc039d8698bf93f815588b3b43c251bd83dd545465' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.3.0@sha256:b7bc1ca4f4d0603a01369a689129aa273a938ce195fe43d00d42f4f2d5212f50' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.0.0@sha256:7e0165f17789192fd4f92efb34aa373450fa859e3b502684b2b121a5582965bf' }
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:1.1.0@sha256:0c1fe4dd6121900624dcb383251ecb0084c3810e095064933de671409d8d6d7b' }
}
};
-35
View File
@@ -1,35 +0,0 @@
'use strict';
exports = module.exports = {
ipFromInt,
intFromIp
};
const assert = require('assert');
function intFromIp(address) {
assert.strictEqual(typeof address, 'string');
const parts = address.split('.');
if (parts.length !== 4) return null;
return (parseInt(parts[0], 10) << (8*3)) & 0xFF000000 |
(parseInt(parts[1], 10) << (8*2)) & 0x00FF0000 |
(parseInt(parts[2], 10) << (8*1)) & 0x0000FF00 |
(parseInt(parts[3], 10) << (8*0)) & 0x000000FF;
}
function ipFromInt(input) {
assert.strictEqual(typeof input, 'number');
let output = [];
for (let i = 3; i >= 0; --i) {
const octet = (input >> (i*8)) & 0x000000FF;
output.push(octet);
}
return output.join('.');
}
+21 -5
View File
@@ -16,15 +16,21 @@ const NOOP_CALLBACK = function () { };
const gConnection = new Docker({ socketPath: '/var/run/docker.sock' });
function cleanupTokens(callback) {
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
function ignoreError(func) {
return function (callback) {
func(function (error) {
if (error) console.error('Ignored error:', error);
callback = callback || NOOP_CALLBACK;
callback();
});
};
}
debug('Cleaning up expired tokens');
function cleanupExpiredTokens(callback) {
assert.strictEqual(typeof callback, 'function');
tokendb.delExpired(function (error, result) {
if (error) return debug('cleanupTokens: error removing expired tokens', error);
if (error) return callback(error);
debug('Cleaned up %s expired tokens.', result);
@@ -32,6 +38,16 @@ function cleanupTokens(callback) {
});
}
function cleanupTokens(callback) {
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
debug('Cleaning up expired tokens');
async.series([
ignoreError(cleanupExpiredTokens)
], callback);
}
function cleanupTmpVolume(containerInfo, callback) {
assert.strictEqual(typeof containerInfo, 'object');
assert.strictEqual(typeof callback, 'function');
+50 -103
View File
@@ -1,11 +1,11 @@
'use strict';
exports = module.exports = {
start,
stop
start: start,
stop: stop
};
const assert = require('assert'),
var assert = require('assert'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
async = require('async'),
@@ -13,13 +13,11 @@ const assert = require('assert'),
constants = require('./constants.js'),
debug = require('debug')('box:ldap'),
eventlog = require('./eventlog.js'),
groups = require('./groups.js'),
ldap = require('ldapjs'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
path = require('path'),
safe = require('safetydance'),
services = require('./services.js'),
users = require('./users.js');
var gServer = null;
@@ -134,8 +132,8 @@ function userSearch(req, res, next) {
var dn = ldap.parseDN('cn=' + user.id + ',ou=users,dc=cloudron');
var memberof = [ GROUP_USERS_DN ];
if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) memberof.push(GROUP_ADMINS_DN);
var groups = [ GROUP_USERS_DN ];
if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) groups.push(GROUP_ADMINS_DN);
var displayName = user.displayName || user.username || ''; // displayName can be empty and username can be null
var nameParts = displayName.split(' ');
@@ -156,7 +154,7 @@ function userSearch(req, res, next) {
givenName: firstName,
username: user.username,
samaccountname: user.username, // to support ActiveDirectory clients
memberof: memberof
memberof: groups
}
};
@@ -288,14 +286,14 @@ function mailboxSearch(req, res, next) {
} else if (req.dn.rdns[0].attrs.domain) { // legacy ldap mailbox search for old sogo
var domain = req.dn.rdns[0].attrs.domain.value.toLowerCase();
mailboxdb.listMailboxes(domain, 1, 1000, function (error, mailboxes) {
mailboxdb.listMailboxes(domain, 1, 1000, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
var results = [];
// send mailbox objects
mailboxes.forEach(function (mailbox) {
result.forEach(function (mailbox) {
var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`);
var obj = {
@@ -329,9 +327,7 @@ function mailboxSearch(req, res, next) {
async.eachSeries(mailboxes, function (mailbox, callback) {
var dn = ldap.parseDN(`cn=${mailbox.name}@${mailbox.domain},ou=mailboxes,dc=cloudron`);
let getFunc = mailbox.ownerType === mail.OWNERTYPE_USER ? users.get : groups.get;
getFunc(mailbox.ownerId, function (error, ownerObject) {
users.get(mailbox.ownerId, function (error, userObject) {
if (error) return callback(); // skip mailboxes with unknown owner
var obj = {
@@ -339,26 +335,30 @@ function mailboxSearch(req, res, next) {
attributes: {
objectclass: ['mailbox'],
objectcategory: 'mailbox',
displayname: mailbox.ownerType === mail.OWNERTYPE_USER ? ownerObject.displayName : ownerObject.name,
displayname: userObject.displayName,
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`
}
};
mailbox.aliases.forEach(function (a, idx) {
obj.attributes['mail' + idx] = `${a.name}@${a.domain}`;
mailboxdb.getAliasesForName(mailbox.name, mailbox.domain, function (error, aliases) {
if (error) return callback(error);
aliases.forEach(function (a, idx) {
obj.attributes['mail' + idx] = `${a}@${mailbox.domain}`;
});
// ensure all filter values are also lowercase
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
results.push(obj);
}
callback();
});
// ensure all filter values are also lowercase
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
results.push(obj);
}
callback();
});
}, function (error) {
if (error) return next(new ldap.OperationsError(error.toString()));
@@ -482,42 +482,18 @@ function authorizeUserForApp(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.app, 'object');
apps.hasAccessTo(req.app, req.user, function (error, hasAccess) {
apps.hasAccessTo(req.app, req.user, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
// we return no such object, to avoid leakage of a users existence
if (!hasAccess) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.upsert(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
res.end();
});
}
function verifyMailboxPassword(mailbox, password, callback) {
assert.strictEqual(typeof mailbox, 'object');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
if (mailbox.ownerType === mail.OWNERTYPE_USER) return users.verify(mailbox.ownerId, password, users.AP_MAIL /* identifier */, callback);
groups.getMembers(mailbox.ownerId, function (error, userIds) {
if (error) return callback(error);
let verifiedUser = null;
async.someSeries(userIds, function iterator(userId, iteratorDone) {
users.verify(userId, password, users.AP_MAIL /* identifier */, function (error, result) {
if (error) return iteratorDone(null, false);
verifiedUser = result;
iteratorDone(null, true);
});
}, function (error, result) {
if (!result) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
callback(null, verifiedUser);
});
});
}
function authenticateUserMailbox(req, res, next) {
debug('user mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
@@ -537,12 +513,12 @@ function authenticateUserMailbox(req, res, next) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
verifyMailboxPassword(mailbox, req.credentials || '', function (error, result) {
users.verify(mailbox.ownerId, req.credentials || '', users.AP_MAIL, function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.upsert(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
});
@@ -571,16 +547,6 @@ function authenticateSftp(req, res, next) {
});
}
function loadSftpConfig(req, res, next) {
services.getServiceConfig('sftp', function (error, serviceConfig) {
if (error) return next(new ldap.OperationsError(error.toString()));
req.requireAdmin = serviceConfig.requireAdmin;
next();
});
}
function userSearchSftp(req, res, next) {
debug('sftp user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
@@ -604,8 +570,6 @@ function userSearchSftp(req, res, next) {
users.getByUsername(username, function (error, user) {
if (error) return next(new ldap.OperationsError(error.toString()));
if (req.requireAdmin && users.compareRoles(user.role, users.ROLE_ADMIN) < 0) return next(new ldap.InsufficientAccessRightsError('Insufficient previleges'));
apps.hasAccessTo(app, user, function (error, hasAccess) {
if (error) return next(new ldap.OperationsError(error.toString()));
if (!hasAccess) return next(new ldap.InsufficientAccessRightsError('Not authorized'));
@@ -613,7 +577,7 @@ function userSearchSftp(req, res, next) {
var obj = {
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
attributes: {
homeDirectory: path.join('/app/data', app.id),
homeDirectory: path.join('/app/data', app.id, 'data'),
objectclass: ['user'],
objectcategory: 'person',
cn: user.id,
@@ -629,37 +593,16 @@ function userSearchSftp(req, res, next) {
});
}
function verifyAppMailboxPassword(addonId, username, password, callback) {
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
const pattern = addonId === 'sendmail' ? 'MAIL_SMTP' : 'MAIL_IMAP';
appdb.getAppIdByAddonConfigValue(addonId, `%${pattern}_PASSWORD`, password, function (error, appId) { // search by password because this is unique for each app
if (error) return callback(error);
appdb.getAddonConfig(appId, addonId, function (error, result) {
if (error) return callback(error);
if (!result.some(r => r.name.endsWith(`${pattern}_USERNAME`) && r.value === username)) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
callback(null);
});
});
}
function authenticateMailAddon(req, res, next) {
debug('mail addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
const parts = email.split('@');
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
if (addonId !== 'sendmail' && addonId !== 'recvmail') return next(new ldap.OperationsError('Invalid DN'));
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
@@ -667,22 +610,26 @@ function authenticateMailAddon(req, res, next) {
if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
verifyAppMailboxPassword(addonId, email, req.credentials || '', function (error) {
if (!error) return res.end(); // validated as app
let namePattern; // manifest v2 has a CLOUDRON_ prefix for names
if (addonId === 'sendmail') namePattern = '%MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') namePattern = '%MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) {
if (error && error.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
if (appId) return res.end();
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
verifyMailboxPassword(mailbox, req.credentials || '', function (error, result) {
users.verify(mailbox.ownerId, req.credentials || '', users.AP_MAIL, function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.upsert(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
});
@@ -698,14 +645,14 @@ function start(callback) {
debug: NOOP,
info: debug,
warn: debug,
error: debug,
fatal: debug
error: console.error,
fatal: console.error
};
gServer = ldap.createServer({ log: logger });
gServer.on('error', function (error) {
debug('start: server error ', error);
console.error('LDAP:', error);
});
gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch);
@@ -713,16 +660,16 @@ function start(callback) {
gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp);
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka (address translation), dovecot (LMTP), sogo (mailbox search)
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka, dovecot
gServer.bind('ou=mailboxes,dc=cloudron', authenticateUserMailbox); // apps like sogo can use domain=${domain} to authenticate a mailbox
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch); // haraka
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch); // haraka
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot (IMAP auth)
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka (MSA auth)
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka
gServer.bind('ou=sftp,dc=cloudron', authenticateSftp); // sftp
gServer.search('ou=sftp,dc=cloudron', loadSftpConfig, userSearchSftp);
gServer.search('ou=sftp,dc=cloudron', userSearchSftp);
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
+79 -225
View File
@@ -1,70 +1,59 @@
'use strict';
exports = module.exports = {
getStatus,
checkConfiguration,
getStatus: getStatus,
checkConfiguration: checkConfiguration,
getLocation,
setLocation, // triggers the change task
changeLocation, // does the actual changing
getDomains: getDomains,
getDomains,
getDomain: getDomain,
clearDomains: clearDomains,
getDomain,
clearDomains,
onDomainAdded: onDomainAdded,
onDomainRemoved: onDomainRemoved,
onDomainAdded,
onDomainRemoved,
removePrivateFields: removePrivateFields,
removePrivateFields,
setDnsRecords: setDnsRecords,
onMailFqdnChanged: onMailFqdnChanged,
setDnsRecords,
validateName: validateName,
validateName,
setMailFromValidation,
setCatchAllAddress,
setMailRelay,
setMailEnabled,
setBanner,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
setMailRelay: setMailRelay,
setMailEnabled: setMailEnabled,
startMail: restartMail,
restartMail,
handleCertChanged,
getMailAuth,
restartMail: restartMail,
handleCertChanged: handleCertChanged,
getMailAuth: getMailAuth,
sendTestMail,
sendTestMail: sendTestMail,
getMailboxCount,
listMailboxes,
getMailbox,
addMailbox,
updateMailboxOwner,
removeMailbox,
listMailboxes: listMailboxes,
removeMailboxes: removeMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailboxOwner: updateMailboxOwner,
removeMailbox: removeMailbox,
getAliases,
setAliases,
getAliases: getAliases,
setAliases: setAliases,
getLists,
getList,
addList,
updateList,
removeList,
resolveList,
getLists: getLists,
getList: getList,
addList: addList,
updateList: updateList,
removeList: removeList,
resolveList: resolveList,
OWNERTYPE_USER: 'user',
OWNERTYPE_GROUP: 'group',
DEFAULT_MEMORY_LIMIT: 512 * 1024 * 1024,
_removeMailboxes: removeMailboxes,
_readDkimPublicKeySync: readDkimPublicKeySync
};
const assert = require('assert'),
var assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
cloudron = require('./cloudron.js'),
constants = require('./constants.js'),
debug = require('debug')('box:mail'),
dns = require('./native-dns.js'),
@@ -80,23 +69,18 @@ const assert = require('assert'),
nodemailer = require('nodemailer'),
path = require('path'),
paths = require('./paths.js'),
request = require('request'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
services = require('./services.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
system = require('./system.js'),
tasks = require('./tasks.js'),
users = require('./users.js'),
validator = require('validator'),
_ = require('underscore');
const DNS_OPTIONS = { timeout: 5000 };
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
const REMOVE_MAILBOX = path.join(__dirname, 'scripts/rmmailbox.sh');
function validateName(name) {
assert.strictEqual(typeof name, 'string');
@@ -116,6 +100,7 @@ function checkOutboundPort25(callback) {
var smtpServer = _.sample([
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.1und1.de',
]);
@@ -252,14 +237,14 @@ function checkSpf(domain, mailFqdn, callback) {
let txtRecord = txtRecords[i].join(''); // https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
if (txtRecord.indexOf('v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecord;
spf.status = spf.value.indexOf(' a:' + settings.mailFqdn()) !== -1;
spf.status = spf.value.indexOf(' a:' + settings.adminFqdn()) !== -1;
break;
}
if (spf.status) {
spf.expected = spf.value;
} else if (i !== txtRecords.length) {
spf.expected = 'v=spf1 a:' + settings.mailFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
spf.expected = 'v=spf1 a:' + settings.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
}
callback(null, spf);
@@ -558,7 +543,7 @@ function checkConfiguration(callback) {
markdownMessage += '\n\n';
});
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://docs.cloudron.io/troubleshooting/#mail-dns) for more information.\n';
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
callback(null, markdownMessage); // empty message means all status checks succeeded
});
@@ -590,18 +575,15 @@ function createMailConfig(mailFqdn, mailDomain, callback) {
}
// create sections for per-domain configuration
async.eachSeries(mailDomains, function (domain, iteratorDone) {
mailDomains.forEach(function (domain) {
const catchAll = domain.catchAll.map(function (c) { return `${c}@${domain.domain}`; }).join(',');
const mailFromValidation = domain.mailFromValidation;
if (!safe.fs.appendFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`[${domain.domain}]\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n\n`, 'utf8')) {
return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
}
if (!safe.fs.writeFileSync(`${paths.ADDON_CONFIG_DIR}/mail/banner/${domain.domain}.text`, domain.banner.text || '')) return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create text banner file:' + safe.error.message));
if (!safe.fs.writeFileSync(`${paths.ADDON_CONFIG_DIR}/mail/banner/${domain.domain}.html`, domain.banner.html || '')) return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create html banner file:' + safe.error.message));
const relay = domain.relay;
const enableRelay = relay.provider !== 'cloudron-smtp' && relay.provider !== 'noop',
@@ -611,26 +593,21 @@ function createMailConfig(mailFqdn, mailDomain, callback) {
username = relay.username || '',
password = relay.password || '';
if (!enableRelay) return iteratorDone();
if (!enableRelay) return;
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=${authType}\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
}
iteratorDone();
}, function (error) {
if (error) return callback(error);
callback(null, mailInDomains.length !== 0 /* allowInbound */);
});
callback(null, mailInDomains.length !== 0 /* allowInbound */);
});
}
function configureMail(mailFqdn, mailDomain, serviceConfig, callback) {
function configureMail(mailFqdn, mailDomain, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof serviceConfig, 'object');
assert.strictEqual(typeof callback, 'function');
// mail (note: 2525 is hardcoded in mail container and app use this port)
@@ -639,8 +616,7 @@ function configureMail(mailFqdn, mailDomain, serviceConfig, callback) {
// mail container uses /app/data for backed up data and /run for restart-able data
const tag = infra.images.mail.tag;
const memoryLimit = serviceConfig.memoryLimit || exports.DEFAULT_MEMORY_LIMIT;
const memory = system.getMemoryAllocation(memoryLimit);
const memoryLimit = 4 * 256;
const cloudronToken = hat(8 * 128), relayToken = hat(8 * 128);
reverseProxy.getCertificate(mailFqdn, mailDomain, function (error, bundle) {
@@ -653,10 +629,7 @@ function configureMail(mailFqdn, mailDomain, serviceConfig, callback) {
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message));
async.series([
shell.exec.bind(null, 'stopMail', 'docker stop mail || true'),
shell.exec.bind(null, 'removeMail', 'docker rm -f mail || true'),
], function (error) {
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
if (error) return callback(error);
createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) {
@@ -671,8 +644,8 @@ function configureMail(mailFqdn, mailDomain, serviceConfig, callback) {
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memory} \
--memory-swap ${memoryLimit} \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MAIL_TOKEN="${cloudronToken}" \
@@ -719,12 +692,8 @@ function restartMail(callback) {
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
services.getServiceConfig('mail', function (error, serviceConfig) {
if (error) return callback(error);
debug(`restartMail: restarting mail container with ${settings.mailFqdn()} ${settings.adminDomain()}`);
configureMail(settings.mailFqdn(), settings.adminDomain(), serviceConfig, callback);
});
debug(`restartMail: restarting mail container with ${settings.mailFqdn()} ${settings.adminDomain()}`);
configureMail(settings.mailFqdn(), settings.adminDomain(), callback);
}
function restartMailIfActivated(callback) {
@@ -777,7 +746,7 @@ function txtRecordsWithSpf(domain, mailFqdn, callback) {
assert.strictEqual(typeof callback, 'function');
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
if (error) return callback(error);
if (error) return error;
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
@@ -918,73 +887,21 @@ function setDnsRecords(domain, callback) {
upsertDnsRecords(domain, settings.mailFqdn(), callback);
}
function getLocation(callback) {
function onMailFqdnChanged(callback) {
assert.strictEqual(typeof callback, 'function');
const domain = settings.mailDomain(), fqdn = settings.mailFqdn();
const subdomain = fqdn.substr(0, fqdn.length - domain.length - 1);
const mailFqdn = settings.mailFqdn(),
mailDomain = settings.adminDomain();
callback(null, { domain, subdomain });
}
function changeLocation(auditSource, progressCallback, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
const fqdn = settings.mailFqdn(), domain = settings.mailDomain();
const subdomain = fqdn.substr(0, fqdn.length - domain.length - 1);
let progress = 20;
progressCallback({ percent: progress, message: `Setting up DNS of certs of mail server ${fqdn}` });
cloudron.setupDnsAndCert(subdomain, domain, auditSource, progressCallback, function (error) {
domains.getAll(function (error, allDomains) {
if (error) return callback(error);
domains.getAll(function (error, allDomains) {
async.eachOfSeries(allDomains, function (domainObject, idx, iteratorDone) {
upsertDnsRecords(domainObject.domain, mailFqdn, iteratorDone);
}, function (error) {
if (error) return callback(error);
async.eachOfSeries(allDomains, function (domainObject, idx, iteratorDone) {
progressCallback({ percent: progress, message: `Updating DNS of ${domainObject.domain}` });
progress += Math.round(70/allDomains.length);
upsertDnsRecords(domainObject.domain, fqdn, function (error) { // ignore any errors. we anyway report dns errors in status tab
progressCallback({ percent: progress, message: `Updated DNS of ${domainObject.domain}: ${error ? error.message : 'success'}` });
iteratorDone();
});
}, function (error) {
if (error) return callback(error);
progressCallback({ percent: 90, message: 'Restarting mail server' });
restartMailIfActivated(callback);
});
});
});
}
function setLocation(subdomain, domain, auditSource, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
const fqdn = domains.fqdn(subdomain, domainObject);
settings.setMailLocation(domain, fqdn, function (error) {
if (error) return callback(error);
tasks.add(tasks.TASK_CHANGE_MAIL_LOCATION, [ auditSource ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
eventlog.add(eventlog.ACTION_MAIL_LOCATION, auditSource, { subdomain, domain, taskId });
callback(null, taskId);
});
configureMail(mailFqdn, mailDomain, callback);
});
});
}
@@ -993,8 +910,6 @@ function onDomainAdded(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!settings.mailFqdn()) return callback(); // mail domain is not set yet (when provisioning)
async.series([
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
@@ -1020,7 +935,7 @@ function clearDomains(callback) {
// remove all fields that should never be sent out via REST API
function removePrivateFields(domain) {
let result = _.pick(domain, 'domain', 'enabled', 'mailFromValidation', 'catchAll', 'relay', 'banner');
let result = _.pick(domain, 'domain', 'enabled', 'mailFromValidation', 'catchAll', 'relay');
if (result.relay.provider !== 'cloudron-smtp') {
if (result.relay.username === result.relay.password) result.relay.username = constants.SECRET_PLACEHOLDER;
result.relay.password = constants.SECRET_PLACEHOLDER;
@@ -1042,20 +957,6 @@ function setMailFromValidation(domain, enabled, callback) {
});
}
function setBanner(domain, banner, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof banner, 'object');
assert.strictEqual(typeof callback, 'function');
maildb.update(domain, { banner }, function (error) {
if (error) return callback(error);
restartMail(NOOP_CALLBACK);
callback(null);
});
}
function setCatchAllAddress(domain, addresses, callback) {
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(addresses));
@@ -1131,25 +1032,13 @@ function sendTestMail(domain, to, callback) {
});
}
function listMailboxes(domain, search, page, perPage, callback) {
function listMailboxes(domain, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listMailboxes(domain, search, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
}
function getMailboxCount(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getMailboxCount(domain, function (error, result) {
mailboxdb.listMailboxes(domain, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
@@ -1179,11 +1068,10 @@ function getMailbox(name, domain, callback) {
});
}
function addMailbox(name, domain, ownerId, ownerType, auditSource, callback) {
function addMailbox(name, domain, userId, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1192,80 +1080,49 @@ function addMailbox(name, domain, ownerId, ownerType, auditSource, callback) {
var error = validateName(name);
if (error) return callback(error);
if (ownerType !== exports.OWNERTYPE_USER && ownerType !== exports.OWNERTYPE_GROUP) return callback(new BoxError(BoxError.BAD_FIELD, 'bad owner type'));
mailboxdb.addMailbox(name, domain, ownerId, ownerType, function (error) {
mailboxdb.addMailbox(name, domain, userId, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, ownerId, ownerType });
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, userId });
callback(null);
});
}
function updateMailboxOwner(name, domain, ownerId, ownerType, auditSource, callback) {
function updateMailboxOwner(name, domain, userId, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
name = name.toLowerCase();
if (ownerType !== exports.OWNERTYPE_USER && ownerType !== exports.OWNERTYPE_GROUP) return callback(new BoxError(BoxError.BAD_FIELD, 'bad owner type'));
getMailbox(name, domain, function (error, result) {
if (error) return callback(error);
mailboxdb.updateMailboxOwner(name, domain, ownerId, ownerType, function (error) {
mailboxdb.updateMailboxOwner(name, domain, userId, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: result.userId, ownerId, ownerType });
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: result.userId, userId });
callback(null);
});
});
}
function removeSolrIndex(mailbox, callback) {
assert.strictEqual(typeof mailbox, 'string');
assert.strictEqual(typeof callback, 'function');
services.getContainerDetails('mail', 'CLOUDRON_MAIL_TOKEN', function (error, addonDetails) {
if (error) return callback(error);
request.post(`https://${addonDetails.ip}:3000/solr_delete_index?access_token=${addonDetails.token}`, { timeout: 2000, rejectUnauthorized: false, json: { mailbox } }, function (error, response) {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new Error(`Error removing solr index - ${response.statusCode} ${JSON.stringify(response.body)}`));
callback(null);
});
});
}
function removeMailbox(name, domain, options, auditSource, callback) {
function removeMailbox(name, domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const mailbox =`${name}@${domain}`;
const deleteMailFunc = options.deleteMails ? shell.sudo.bind(null, 'removeMailbox', [ REMOVE_MAILBOX, mailbox ], {}) : (next) => next();
mailboxdb.del(name, domain, function (error) {
if (error) return callback(error);
deleteMailFunc(function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error removing mailbox: ${error.message}`));
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
mailboxdb.del(name, domain, function (error) {
if (error) return callback(error);
removeSolrIndex(mailbox, NOOP_CALLBACK);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
callback();
});
callback(null);
});
}
@@ -1308,14 +1165,11 @@ function setAliases(name, domain, aliases, callback) {
});
}
function getLists(domain, search, page, perPage, callback) {
function getLists(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getLists(domain, search, page, perPage, function (error, result) {
mailboxdb.getLists(domain, function (error, result) {
if (error) return callback(error);
callback(null, result);
@@ -1442,7 +1296,7 @@ function resolveList(listName, listDomain, callback) {
if (entry.type === mailboxdb.TYPE_MAILBOX) { // concrete mailbox
result.push(member);
} else if (entry.type === mailboxdb.TYPE_ALIAS) { // resolve aliases
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasDomain}`);
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasTarget}`);
} else { // resolve list members
toResolve = toResolve.concat(entry.members);
}
+2 -2
View File
@@ -7,8 +7,8 @@ The application '<%= title %>' installed at <%= appFqdn %> is not responding.
This is most likely a problem in the application.
To resolve this, you can try the following:
* Restart the app by opening the app's web terminal - https://docs.cloudron.io/apps/#web-terminal
* Restore the app to the latest backup - https://docs.cloudron.io/backups/#restoring-an-app
* Restart the app by opening the app's web terminal - https://cloudron.io/documentation/apps/#web-terminal
* Restore the app to the latest backup - https://cloudron.io/documentation/backups/#restoring-an-app
* Contact us via <%= supportEmail %> or https://forum.cloudron.io
+12 -4
View File
@@ -3,14 +3,18 @@
Dear Cloudron Admin,
<% for (var i = 0; i < apps.length; i++) { -%>
The app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> has an update available.
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> is available.
<%= apps[i].app.manifest.title %> v<%= apps[i].updateInfo.manifest.version %> changes:
Changes:
<%= apps[i].updateInfo.manifest.changelog %>
<% } -%>
<% if (!hasSubscription) { -%>
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
<% } else { -%>
Update now at <%= webadminUrl %>
<% } -%>
Powered by https://cloudron.io
@@ -29,20 +33,24 @@ Sent at: <%= new Date().toUTCString() %>
<div style="width: 650px; text-align: left;">
<% for (var i = 0; i < apps.length; i++) { -%>
<p>
The app '<%= apps[i].app.manifest.title %>' installed at <a href="https://<%= apps[i].app.fqdn %>"><%= apps[i].app.fqdn %></a> has an update available.
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <a href="https://<%= apps[i].app.fqdn %>"><%= apps[i].app.fqdn %></a> is available.
</p>
<h5><%= apps[i].app.manifest.title %> v<%= apps[i].updateInfo.manifest.version %> changes:</h5>
<h5>Changelog:</h5>
<%- apps[i].changelogHTML %>
<br/>
<% } -%>
<% if (!hasSubscription) { -%>
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
<% } else { -%>
<p>
<br/>
<center><a href="<%= webadminUrl %>">Update now</a></center>
<br/>
</p>
<% } -%>
</div>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
+1 -1
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
Cloudron failed to create a complete backup. Please see https://docs.cloudron.io/troubleshooting/#backups
Cloudron failed to create a complete backup. Please see https://cloudron.io/documentation/troubleshooting/#backups
for troubleshooting.
Logs for this failure are available at <%= logUrl %>
@@ -1,45 +0,0 @@
<%if (format === 'text') { %>
Dear <%= cloudronName %> Admin,
Cloudron v<%= newBoxVersion %> is now available!
Changes:
<% for (var i = 0; i < changelog.length; i++) { %>
* <%- changelog[i] %>
<% } %>
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Dear <%= cloudronName %> Admin,</h3>
<div style="width: 650px; text-align: left;">
<p>
Cloudron v<%= newBoxVersion %> is now available!
</p>
<h5>Changes:</h5>
<ul>
<% for (var i = 0; i < changelogHTML.length; i++) { %>
<li><%- changelogHTML[i] %></li>
<% } %>
</ul>
<br/>
</div>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>.
</div>
</center>
<% } %>
@@ -8,7 +8,7 @@ The Cloudron will attempt to renew the certificate every 12 hours
until the certificate expires (at which point it will switch to
using the fallback certificate).
See https://docs.cloudron.io/troubleshooting/#certificates to
See https://cloudron.io/documentation/troubleshooting/#certificates to
double check if your server is configured correctly to obtain certificates
via Let's Encrypt.
+6 -7
View File
@@ -2,13 +2,12 @@
Dear <%= cloudronName %> Admin,
<%if (app) { %>
The application at <%= app.fqdn %> ran out of memory. The application has been restarted automatically. If you see this notification often,
consider increasing the memory limit - <%= webadminUrl %>/#/app/<%= app.id %>/resources .
<% } else { %>
The addon <%= addon.name %> service ran out of memory. The service has been restarted automatically. If you see this notification often,
consider increasing the memory limit - <%= webadminUrl %>/#/services .
<% } %>
<%= program %> was restarted now as it ran out of memory.
If this message appears repeatedly, give the app more memory.
* To increase an app's memory limit - https://cloudron.io/documentation/apps/#memory-limit
* To increase a service's memory limit - https://cloudron.io/documentation/troubleshooting/#services
Out of memory event:
@@ -1,24 +0,0 @@
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>{{ passwordResetEmail.salutation }}</h3>
<p>{{ passwordResetEmail.description }}</p>
<p>
<a href="<%= resetLink %>">{{ passwordResetEmail.resetAction }}</a>
</p>
<br/>
{{ passwordResetEmail.expireNote }}
<br/>
<br/>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>
</div>
</center>
@@ -1,9 +0,0 @@
{{ passwordResetEmail.salutation }}
{{ passwordResetEmail.description }}
{{ passwordResetEmail.resetActionText }}
{{ passwordResetEmail.expireNote }}
Powered by https://cloudron.io
+41
View File
@@ -0,0 +1,41 @@
<%if (format === 'text') { %>
Hi <%= user.displayName || user.username || user.email %>,
Someone, hopefully you, has requested your account's password
be reset. If you did not request this reset, please ignore this message.
To reset your password, please visit the following page:
<%- resetLink %>
Powered by https://cloudron.io
<% } else { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
<p>
Someone, hopefully you, has requested your account's password be reset.<br/>
If you did not request this reset, please ignore this message.
</p>
<p>
<a href="<%= resetLink %>">Click to reset your password</a>
</p>
<br/>
<br/>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>
</div>
</center>
<% } %>
+30
View File
@@ -0,0 +1,30 @@
<%if (format === 'text') { %>
Dear <%= cloudronName %> Admin,
A new user with email <%= user.email %> was added to <%= cloudronName %>.
Powered by https://cloudron.io
<% } else { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Dear <%= cloudronName %> Admin,</h3>
<p>
A new user with email <%= user.email %> was added to <%= cloudronName %>.
</p>
<br/>
<br/>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>.
</div>
</center>
<% } %>
+14
View File
@@ -0,0 +1,14 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
User <%= user.username || user.email %> <%= event %>.
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<% } %>
-28
View File
@@ -1,28 +0,0 @@
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>{{ welcomeEmail.salutation }}</h3>
<h2>{{ welcomeEmail.welcomeTo }}</h2>
<p>
<a href="<%= inviteLink %>">{{ welcomeEmail.inviteLinkAction }}</a>
</p>
<br/>
<br/>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
<% if (invitor) { -%>
{{ welcomeEmail.invitor }}
<% } -%>
<br/>
{{ welcomeEmail.expireNote }}
<br/>
Powered by <a href="https://cloudron.io">Cloudron</a>
</div>
</center>
-13
View File
@@ -1,13 +0,0 @@
{{ welcomeEmail.salutation }}
{{ welcomeEmail.welcomeTo }}
{{ welcomeEmail.inviteLinkActionText }}
<% if (invitor) { %>
{{ welcomeEmail.invitor }}
<% } %>
{{ welcomeEmail.expireNote }}
Powered by https://cloudron.io
+46
View File
@@ -0,0 +1,46 @@
<%if (format === 'text') { %>
Dear <%= user.displayName || user.username || user.email %>,
Welcome to <%= cloudronName %>!
Follow the link to get started.
<%- inviteLink %>
<% if (invitor && invitor.email) { %>
You are receiving this email because you were invited by <%= invitor.email %>.
<% } %>
Powered by https://cloudron.io
<% } else { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
<h2>Welcome to <%= cloudronName %>!</h2>
<p>
<a href="<%= inviteLink %>">Get started</a>.
</p>
<br/>
<br/>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
<% if (invitor && invitor.email) { %>
You are receiving this email because you were invited by <%= invitor.email %>.
<% } %>
<br/>
Powered by <a href="https://cloudron.io">Cloudron</a>
</div>
</center>
<% } %>
+47 -97
View File
@@ -1,32 +1,31 @@
'use strict';
exports = module.exports = {
addMailbox,
addList,
addMailbox: addMailbox,
addList: addList,
updateMailboxOwner,
updateList,
del,
updateMailboxOwner: updateMailboxOwner,
updateList: updateList,
del: del,
getMailboxCount,
listMailboxes,
getLists,
listMailboxes: listMailboxes,
getLists: getLists,
listAllMailboxes,
listAllMailboxes: listAllMailboxes,
get,
getMailbox,
getList,
getAlias,
get: get,
getMailbox: getMailbox,
getList: getList,
getAlias: getAlias,
getAliasesForName,
setAliasesForName,
getAliasesForName: getAliasesForName,
setAliasesForName: setAliasesForName,
getByOwnerId,
delByOwnerId,
delByDomain,
getByOwnerId: getByOwnerId,
delByOwnerId: delByOwnerId,
delByDomain: delByDomain,
updateName,
updateName: updateName,
_clear: clear,
@@ -38,11 +37,10 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
mysql = require('mysql'),
safe = require('safetydance'),
util = require('util');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain' ].join(',');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain' ].join(',');
function postProcess(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ];
@@ -53,24 +51,13 @@ function postProcess(data) {
return data;
}
function postProcessAliases(data) {
const aliasNames = JSON.parse(data.aliasNames), aliasDomains = JSON.parse(data.aliasDomains);
delete data.aliasNames;
delete data.aliasDomains;
data.aliases = [];
for (let i = 0; i < aliasNames.length; i++) { // NOTE: aliasNames is [ null ] when no aliases
if (aliasNames[i]) data.aliases[i] = { name: aliasNames[i], domain: aliasDomains[i] };
}
}
function addMailbox(name, domain, ownerId, ownerType, callback) {
function addMailbox(name, domain, ownerId, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType ], function (error) {
database.query('INSERT INTO mailboxes (name, type, domain, ownerId) VALUES (?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
@@ -78,14 +65,13 @@ function addMailbox(name, domain, ownerId, ownerType, callback) {
});
}
function updateMailboxOwner(name, domain, ownerId, ownerType, callback) {
function updateMailboxOwner(name, domain, ownerId, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE mailboxes SET ownerId = ?, ownerType = ? WHERE name = ? AND domain = ?', [ ownerId, ownerType, name, domain ], function (error, result) {
database.query('UPDATE mailboxes SET ownerId = ? WHERE name = ? AND domain = ?', [ ownerId, name, domain ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
@@ -100,8 +86,8 @@ function addList(name, domain, members, membersOnly, callback) {
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson, membersOnly) VALUES (?, ?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', 'user', JSON.stringify(members), membersOnly ], function (error) {
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson, membersOnly) VALUES (?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members), membersOnly ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
@@ -217,43 +203,20 @@ function getMailbox(name, domain, callback) {
});
}
function getMailboxCount(domain, callback) {
function listMailboxes(domain, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM mailboxes WHERE type = ? AND domain = ?', [ exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results[0].total);
});
}
function listMailboxes(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
const escapedSearch = mysql.escape('%' + search + '%'); // this also quotes the string
const searchQuery = search ? ` HAVING (name LIKE ${escapedSearch} OR aliasNames LIKE ${escapedSearch} OR aliasDomains LIKE ${escapedSearch})` : ''; // having instead of where because of aggregated columns use
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains '
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
+ ' WHERE m1.domain = ?'
+ ' GROUP BY m1.name, m1.domain, m1.ownerId'
+ searchQuery
+ ' ORDER BY name LIMIT ?,?';
results.forEach(function (result) { postProcess(result); });
database.query(query, [ domain, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(postProcessAliases);
callback(null, results);
});
callback(null, results);
});
}
function listAllMailboxes(page, perPage, callback) {
@@ -261,41 +224,28 @@ function listAllMailboxes(page, perPage, callback) {
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains '
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1` +
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2` +
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
+ ' GROUP BY m1.name, m1.domain, m1.ownerId'
+ ' ORDER BY name LIMIT ?,?';
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ exports.TYPE_MAILBOX ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
database.query(query, [ (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
results.forEach(postProcessAliases);
callback(null, results);
});
callback(null, results);
});
}
function getLists(domain, search, page, perPage, callback) {
function getLists(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ' OR membersJson LIKE ' + mysql.escape('%' + search + '%') + ')';
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND domain = ?',
[ exports.TYPE_LIST, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
query += 'ORDER BY name LIMIT ?,?';
results.forEach(function (result) { postProcess(result); });
database.query(query, [ exports.TYPE_LIST, domain, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
callback(null, results);
});
}
function getList(name, domain, callback) {
@@ -340,8 +290,8 @@ function setAliasesForName(name, domain, aliases, callback) {
// clear existing aliases
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId, results[0].ownerType ] });
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId) VALUES (?, ?, ?, ?, ?, ?)',
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId ] });
});
database.transaction(queries, function (error) {
+3 -6
View File
@@ -17,7 +17,7 @@ var assert = require('assert'),
database = require('./database.js'),
safe = require('safetydance');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector', 'bannerJson' ].join(',');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector' ].join(',');
function postProcess(data) {
data.enabled = !!data.enabled; // int to boolean
@@ -29,9 +29,6 @@ function postProcess(data) {
data.relay = safe.JSON.parse(data.relayJson) || { provider: 'cloudron-smtp' };
delete data.relayJson;
data.banner = safe.JSON.parse(data.bannerJson) || { text: null, html: null };
delete data.bannerJson;
return data;
}
@@ -77,8 +74,8 @@ function update(domain, data, callback) {
var args = [ ];
var fields = [ ];
for (var k in data) {
if (k === 'catchAll' || k === 'banner') {
fields.push(`${k}Json = ?`);
if (k === 'catchAll') {
fields.push('catchAllJson = ?');
args.push(JSON.stringify(data[k]));
} else if (k === 'relay') {
fields.push('relayJson = ?');
+142 -119
View File
@@ -1,23 +1,25 @@
'use strict';
exports = module.exports = {
passwordReset,
boxUpdateAvailable,
appUpdatesAvailable,
userAdded: userAdded,
userRemoved: userRemoved,
roleChanged: roleChanged,
passwordReset: passwordReset,
appUpdatesAvailable: appUpdatesAvailable,
sendInvite,
sendInvite: sendInvite,
appUp,
appDied,
appUpdated,
oomEvent,
appUp: appUp,
appDied: appDied,
appUpdated: appUpdated,
oomEvent: oomEvent,
backupFailed,
backupFailed: backupFailed,
certificateRenewalError,
boxUpdateError,
certificateRenewalError: certificateRenewalError,
boxUpdateError: boxUpdateError,
sendTestMail,
sendTestMail: sendTestMail,
_mailQueue: [] // accumulate mails in test mode
};
@@ -32,8 +34,8 @@ var assert = require('assert'),
safe = require('safetydance'),
settings = require('./settings.js'),
showdown = require('showdown'),
translation = require('./translation.js'),
smtpTransport = require('nodemailer-smtp-transport');
smtpTransport = require('nodemailer-smtp-transport'),
util = require('util');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -89,21 +91,14 @@ function sendMail(mailOptions, callback) {
});
}
function render(templateFile, params, translationAssets) {
function render(templateFile, params) {
assert.strictEqual(typeof templateFile, 'string');
assert.strictEqual(typeof params, 'object');
var content = null;
var raw = safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8');
if (raw === null) {
debug(`Error loading ${templateFile}`);
return '';
}
if (typeof translationAssets === 'object') raw = translation.translate(raw, translationAssets.translations || {}, translationAssets.fallback || {});
try {
content = ejs.render(raw, params);
content = ejs.render(safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8'), params);
} catch (e) {
debug(`Error rendering ${templateFile}`, e);
}
@@ -111,6 +106,25 @@ function render(templateFile, params, translationAssets) {
return content;
}
function mailUserEvent(mailTo, user, event) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof event, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: util.format('[%s] %s %s', mailConfig.cloudronName, user.username || user.fallbackEmail || user.email, event),
text: render('user_event.ejs', { user: user, event: event, format: 'text' }),
};
sendMail(mailOptions);
});
}
function sendInvite(user, invitor, inviteLink) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof invitor, 'object');
@@ -121,31 +135,84 @@ function sendInvite(user, invitor, inviteLink) {
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
translation.getTranslations(function (error, translationAssets) {
if (error) return debug('Error getting translations:', error);
var templateData = {
user: user,
webadminUrl: settings.adminOrigin(),
inviteLink: inviteLink,
invitor: invitor,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateData = {
user: user.displayName || user.username || user.email,
webadminUrl: settings.adminOrigin(),
inviteLink: inviteLink,
invitor: invitor ? invitor.email : null,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: ejs.render(translation.translate('{{ welcomeEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
text: render('welcome_user-text.ejs', templateData, translationAssets),
html: render('welcome_user-html.ejs', templateData, translationAssets)
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
sendMail(mailOptions);
});
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: util.format('Welcome to %s', mailConfig.cloudronName),
text: render('welcome_user.ejs', templateDataText),
html: render('welcome_user.ejs', templateDataHTML)
};
sendMail(mailOptions);
});
}
function userAdded(mailTo, user) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
debug(`userAdded: Sending mail for added users ${user.fallbackEmail} to ${mailTo}`);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var templateData = {
user: user,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: util.format('[%s] User %s added', mailConfig.cloudronName, user.fallbackEmail),
text: render('user_added.ejs', templateDataText),
html: render('user_added.ejs', templateDataHTML)
};
sendMail(mailOptions);
});
}
function userRemoved(mailTo, user) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
debug('Sending mail for userRemoved.', user.id, user.username, user.email);
mailUserEvent(mailTo, user, 'was removed');
}
function roleChanged(mailTo, user) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
debug('Sending mail for roleChanged');
mailUserEvent(mailTo, user, `now has the role '${user.role}'`);
}
function passwordReset(user) {
assert.strictEqual(typeof user, 'object');
@@ -154,26 +221,28 @@ function passwordReset(user) {
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
translation.getTranslations(function (error, translationAssets) {
if (error) return debug('Error getting translations:', error);
var templateData = {
user: user,
resetLink: `${settings.adminOrigin()}/login.html?resetToken=${user.resetToken}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateData = {
user: user.displayName || user.username || user.email,
resetLink: `${settings.adminOrigin()}/login.html?resetToken=${user.resetToken}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: ejs.render(translation.translate('{{ passwordResetEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
text: render('password_reset-text.ejs', templateData, translationAssets),
html: render('password_reset-html.ejs', templateData, translationAssets)
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
sendMail(mailOptions);
});
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: util.format('[%s] Password Reset', mailConfig.cloudronName),
text: render('password_reset.ejs', templateDataText),
html: render('password_reset.ejs', templateDataHTML)
};
sendMail(mailOptions);
});
}
@@ -189,7 +258,7 @@ function appUp(mailTo, app) {
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] App ${app.fqdn} is back online`,
subject: util.format('[%s] App %s is back online', mailConfig.cloudronName, app.fqdn),
text: render('app_up.ejs', { title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
};
@@ -209,7 +278,7 @@ function appDied(mailTo, app) {
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] App ${app.fqdn} is down`,
subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn),
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: mailConfig.supportEmail, format: 'text' })
};
@@ -256,46 +325,10 @@ function appUpdated(mailTo, app, callback) {
});
}
function boxUpdateAvailable(mailTo, updateInfo, callback) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof updateInfo, 'object');
assert.strictEqual(typeof callback, 'function');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var converter = new showdown.Converter();
var templateData = {
webadminUrl: settings.adminOrigin(),
newBoxVersion: updateInfo.version,
changelog: updateInfo.changelog,
changelogHTML: updateInfo.changelog.map(function (e) { return converter.makeHtml(e); }),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] Cloudron update available`,
text: render('box_update_available.ejs', templateDataText),
html: render('box_update_available.ejs', templateDataHTML)
};
sendMail(mailOptions, callback);
});
}
function appUpdatesAvailable(mailTo, apps, callback) {
function appUpdatesAvailable(mailTo, apps, hasSubscription, callback) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof apps, 'object');
assert.strictEqual(typeof hasSubscription, 'boolean');
assert.strictEqual(typeof callback, 'function');
getMailConfig(function (error, mailConfig) {
@@ -308,6 +341,7 @@ function appUpdatesAvailable(mailTo, apps, callback) {
var templateData = {
webadminUrl: settings.adminOrigin(),
hasSubscription: hasSubscription,
apps: apps,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
@@ -322,7 +356,7 @@ function appUpdatesAvailable(mailTo, apps, callback) {
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] App update available`,
subject: `New app updates available for ${mailConfig.cloudronName}`,
text: render('app_updates_available.ejs', templateDataText),
html: render('app_updates_available.ejs', templateDataHTML)
};
@@ -340,7 +374,7 @@ function backupFailed(mailTo, errorMessage, logUrl) {
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] Failed to backup`,
subject: util.format('[%s] Failed to backup', mailConfig.cloudronName),
text: render('backup_failed.ejs', { cloudronName: mailConfig.cloudronName, message: errorMessage, logUrl: logUrl, format: 'text' })
};
@@ -359,7 +393,7 @@ function certificateRenewalError(mailTo, domain, message) {
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] Certificate renewal error`,
subject: util.format('[%s] Certificate renewal error', domain),
text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' })
};
@@ -377,7 +411,7 @@ function boxUpdateError(mailTo, message) {
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] Cloudron update error`,
subject: util.format('[%s] Cloudron update error', mailConfig.cloudronName),
text: render('box_update_error.ejs', { message: message, format: 'text' })
};
@@ -385,30 +419,19 @@ function boxUpdateError(mailTo, message) {
});
}
function oomEvent(mailTo, app, addon, containerId, event) {
function oomEvent(mailTo, program, event) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addon, 'object');
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof event, 'object');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
const templateData = {
webadminUrl: settings.adminOrigin(),
cloudronName: mailConfig.cloudronName,
app,
addon,
event: JSON.stringify(event),
format: 'text'
};
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] ${app ? app.fqdn : addon.name} was restarted (OOM)`,
text: render('oom_event.ejs', templateData)
subject: util.format('[%s] %s was restarted (OOM)', mailConfig.cloudronName, program),
text: render('oom_event.ejs', { cloudronName: mailConfig.cloudronName, program: program, event: JSON.stringify(event), format: 'text' })
};
sendMail(mailOptions);
@@ -427,7 +450,7 @@ function sendTestMail(domain, email, callback) {
authUser: `no-reply@${domain}`,
from: `"${mailConfig.cloudronName}" <no-reply@${domain}>`,
to: email,
subject: `[${mailConfig.cloudronName}] Test Email`,
subject: util.format('Test Email from %s', mailConfig.cloudronName),
text: render('test.ejs', { cloudronName: mailConfig.cloudronName, format: 'text'})
};

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