Compare commits

..

1 Commits

Author SHA1 Message Date
Johannes Zellner 6b65d3016d Add 5.4.1 changes 2020-07-19 19:27:31 +02:00
119 changed files with 2125 additions and 2597 deletions
-71
View File
@@ -2045,74 +2045,3 @@
* 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
[6.0.0]
* Focal support
+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/)
+6 -12
View File
@@ -30,7 +30,6 @@ 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
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 \
acl \
build-essential \
@@ -40,12 +39,11 @@ apt-get -y install \
debconf-utils \
dmsetup \
$gpg_package \
ipset \
iptables \
libpython2.7 \
linux-generic \
logrotate \
$mysql_package \
mysql-server-5.7 \
openssh-server \
pwgen \
resolvconf \
@@ -84,9 +82,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
@@ -123,8 +121,6 @@ if ! apt-get install -y libcurl3-gnutls collectd collectd-utils; then
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
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
@@ -133,13 +129,11 @@ timedatectl set-ntp 1
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 and conflicts with unbound)
systemctl stop bind9 || true
+39 -43
View File
@@ -2,61 +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'),
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,
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.');
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.');
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,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();
};
+2 -1
View File
@@ -66,6 +66,8 @@ CREATE TABLE IF NOT EXISTS apps(
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
@@ -161,7 +163,6 @@ CREATE TABLE IF NOT EXISTS mail(
mailFromValidation BOOLEAN DEFAULT 1,
catchAllJson TEXT,
relayJson TEXT,
bannerJson TEXT,
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
+293 -275
View File
@@ -5,9 +5,9 @@
"requires": true,
"dependencies": {
"@google-cloud/common": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.4.0.tgz",
"integrity": "sha512-zWFjBS35eI9leAHhjfeOYlK5Plcuj/77EzstnrJIZbKgF/nkqjcQuGiMCpzCwOfPyUbz8ZaEOYgbHa759AKbjg==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.3.0.tgz",
"integrity": "sha512-nmIyi3q/FL2j6ZJ61xK/863DoJEZayI2/W/iCgwrCYUYsem277XO45MBTAimjgiKBCA0c9InmQyfT48h/IK4jg==",
"requires": {
"@google-cloud/projectify": "^1.0.0",
"@google-cloud/promisify": "^1.0.0",
@@ -21,15 +21,15 @@
}
},
"@google-cloud/dns": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@google-cloud/dns/-/dns-1.2.9.tgz",
"integrity": "sha512-5TLLs0d3pXetWD7H/DWnGA+rQwBmjXj+XOWDe79/cDo3IihEueBK5YrWXgfBRYgTT7vsQnDkP7pdfDkcI0Wnag==",
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@google-cloud/dns/-/dns-1.2.8.tgz",
"integrity": "sha512-4dDYUPJHjxYPtx6ioTht5DoHHZC3lM3cKUTxxMWCBcczHpGYxfSMhj5zfiWctULcBhorW5ufHz4+HZHNwspHvg==",
"requires": {
"@google-cloud/common": "^2.0.0",
"@google-cloud/paginator": "^2.0.0",
"@google-cloud/promisify": "^1.0.0",
"arrify": "^2.0.0",
"dns-zonefile": "0.2.6",
"dns-zonefile": "0.2.3",
"lodash.groupby": "^4.6.0",
"string-format-obj": "^1.1.1"
}
@@ -262,11 +262,6 @@
"execa": "^2.0.1"
}
},
"@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
"integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="
},
"@types/caseless": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
@@ -321,11 +316,6 @@
"event-target-shim": "^5.0.0"
}
},
"abstract-logging": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.0.tgz",
"integrity": "sha512-/oA9z7JszpIioo6J6dB79LVUgJ3eD3cxkAmdCkvWWS+Y9tPtALs1rLqOekLUXUbYqM2fB9TTK0ibAyZJJOP/CA=="
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@@ -336,12 +326,9 @@
}
},
"agent-base": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz",
"integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==",
"requires": {
"debug": "4"
}
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
"integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g=="
},
"ajv": {
"version": "6.12.2",
@@ -458,11 +445,11 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"aws-sdk": {
"version": "2.759.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.759.0.tgz",
"integrity": "sha512-XTmt6b8NfluqmixO18Bu9ZttZMW9rwEDVO+XITsIQ5dZvLectwrjlbEn2refudJI1Y2pxLguw+tABvXi1wtbbQ==",
"version": "2.685.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.685.0.tgz",
"integrity": "sha512-mAOj7b4PuXRxIZkNdSkBWZ28lS2wYUY7O9u33nH9a7BawlttMNbxOgE/wDCPMrTLfj+RLQx0jvoIYj8BKCTRFw==",
"requires": {
"buffer": "4.9.2",
"buffer": "4.9.1",
"events": "1.1.1",
"ieee754": "1.1.13",
"jmespath": "0.15.0",
@@ -501,7 +488,7 @@
},
"backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"resolved": false,
"integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
"requires": {
"precond": "0.2"
@@ -614,9 +601,9 @@
"dev": true
},
"buffer": {
"version": "4.9.2",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
"integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4",
@@ -652,6 +639,17 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
"bunyan": {
"version": "1.8.12",
"resolved": false,
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
"requires": {
"dtrace-provider": "~0.8",
"moment": "^2.10.6",
"mv": "~2",
"safe-json-stringify": "~1"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -743,9 +741,9 @@
}
},
"cloudron-manifestformat": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.6.0.tgz",
"integrity": "sha512-BqM2vw/OWUHmPmrQo3xwAME0ncX3JPmPtxrhOYy0ZRpNcRDLrwXz02WVM9hAvIoawJNJjVb+x22RQoa1y5DdMw==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.5.0.tgz",
"integrity": "sha512-Xf1vOwCFT5h1MZQ9fC8EyfL2jfpVlShg5r7est/ZA+vSzcbvk2nQxPmpk4q4e6iDfr19B7iUw2b2X7mw5c1Dlg==",
"requires": {
"cron": "^1.8.2",
"java-packagename-regex": "^1.0.0",
@@ -1015,9 +1013,9 @@
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz",
"integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==",
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -1218,22 +1216,22 @@
}
},
"db-migrate-base": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/db-migrate-base/-/db-migrate-base-2.3.0.tgz",
"integrity": "sha512-mxaCkSe7JC2uksvI/rKs+wOQGBSZ6B87xa4b3i+QhB+XRBpGdpMzldKE6INf+EnM6kwhbIPKjyJZgyxui9xBfQ==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/db-migrate-base/-/db-migrate-base-1.6.3.tgz",
"integrity": "sha512-O6Kh72Yh0DfvRAKg9QKzu1KkMwI5iI0dFHO6RmDLvbiYvtRW3faFXJNFwJYeioVBx22QA3AkVFbDIcw4j4QxGw==",
"requires": {
"bluebird": "^3.1.1"
}
},
"db-migrate-mysql": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/db-migrate-mysql/-/db-migrate-mysql-2.1.1.tgz",
"integrity": "sha512-dqyH+N8NyfDP1NSUHmkOuUDBhscTt3obubJ3VKz4BRyKtB/NOdx/hKxREV/rpytFGZbI2LT+ELeINSXU8N5IKQ==",
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/db-migrate-mysql/-/db-migrate-mysql-1.1.10.tgz",
"integrity": "sha1-TB1e8R9ZO1qPHxIkkIO+5xWnzKw=",
"requires": {
"bluebird": "^3.2.1",
"db-migrate-base": "^2.1.1",
"db-migrate-base": "^1.2.5",
"moment": "^2.11.2",
"mysql": "^2.17.1"
"mysql": "^2.10.2"
}
},
"db-migrate-shared": {
@@ -1242,11 +1240,11 @@
"integrity": "sha512-65k86bVeHaMxb2L0Gw3y5V+CgZSRwhVQMwDMydmw5MvIpHHwD6SmBciqIwHsZfzJ9yzV/yYhdRefRM6FV5/siw=="
},
"debug": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "2.1.2"
"ms": "^2.1.1"
}
},
"decamelize": {
@@ -1315,9 +1313,9 @@
"integrity": "sha1-082BIh4+pAdCz83lVtTpnpjdxxs="
},
"dns-zonefile": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/dns-zonefile/-/dns-zonefile-0.2.6.tgz",
"integrity": "sha512-8hkrtLbVNqCgnRQv8jjit8MnGzqYBouxoP/WMAObbN0aPr43hy/Ml+VxMXKC75lRz2DEwUFN2SNpVnrrQWobew=="
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/dns-zonefile/-/dns-zonefile-0.2.3.tgz",
"integrity": "sha512-ijQM13UI3a1uFvN4IB0LNesgBdYhzJugSzpiEzfeIgZmPNvkTkO/U7Z5sjwxBGuhCc3vCClOhA762bGQchmr1Q=="
},
"docker-modem": {
"version": "1.0.9",
@@ -1431,6 +1429,15 @@
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz",
"integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow=="
},
"dtrace-provider": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
"integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
"optional": true,
"requires": {
"nan": "^2.14.0"
}
},
"duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@@ -1470,9 +1477,9 @@
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
},
"ejs-cli": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ejs-cli/-/ejs-cli-2.2.1.tgz",
"integrity": "sha512-rLCGrRtTRKVJgZpv5HrVXnhJdi8EJSslQki/0WZqSC52m1njdcQCvsm5fiP/IKErPB1j8Sf9FzFm6hIwdpC4Tw==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ejs-cli/-/ejs-cli-2.2.0.tgz",
"integrity": "sha512-siC0o+kz6zM5CUNbm08FLj6LeyVoIOAccc1Ze3uOBAYpK7leP5WJpJPLCMBVtivSkH5BZaqPU8XpgP0ffjVteA==",
"requires": {
"async": "^3.2.0",
"colors": "^1.4.0",
@@ -1722,6 +1729,14 @@
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz",
"integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ=="
},
"fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
"requires": {
"pend": "~1.2.0"
}
},
"final-fs": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/final-fs/-/final-fs-1.6.1.tgz",
@@ -1812,38 +1827,6 @@
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"fs-extra": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.6.4.tgz",
"integrity": "sha1-9G8MdbeEH40gCzNIzU1pHVoJnRU=",
"dev": true,
"requires": {
"jsonfile": "~1.0.1",
"mkdirp": "0.3.x",
"ncp": "~0.4.2",
"rimraf": "~2.2.0"
},
"dependencies": {
"mkdirp": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=",
"dev": true
},
"ncp": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz",
"integrity": "sha1-q8xsvT7C7Spyn/bnwfqPAXhKhXQ=",
"dev": true
},
"rimraf": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=",
"dev": true
}
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -1921,13 +1904,13 @@
}
},
"gaxios": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz",
"integrity": "sha512-US8UMj8C5pRnao3Zykc4AAVr+cffoNKRTg9Rsf2GiuZCW69vgJj38VK2PzlPuQU73FZ/nTk9/Av6/JGcE1N9vA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.0.tgz",
"integrity": "sha512-VgC4JKJQAAAGK5rFZbPcS5mXsdIYVMIUJOxMjSOkYdfhB74R0L6y8PFQDdS0r1ObG6hdP11e71EjHh3xbI+6fQ==",
"requires": {
"abort-controller": "^3.0.0",
"extend": "^3.0.2",
"https-proxy-agent": "^5.0.0",
"https-proxy-agent": "^4.0.0",
"is-stream": "^2.0.0",
"node-fetch": "^2.3.0"
}
@@ -1942,9 +1925,9 @@
}
},
"gcp-metadata": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.5.0.tgz",
"integrity": "sha512-ZQf+DLZ5aKcRpLzYUyBS3yo3N0JSa82lNDO8rj3nMSlovLcz2riKFBsYgDzeXcv75oo5eqB2lx+B14UvPoCRnA==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.3.1.tgz",
"integrity": "sha512-RrASg1HaVAxoB9Q/8sYfJ++v9PMiiqIgOrOxZeagMgS4osZtICT1lKBx2uvzYgwetxj8i6K99Z0iuKMg7WraTg==",
"requires": {
"gaxios": "^2.1.0",
"json-bigint": "^0.3.0"
@@ -2112,9 +2095,9 @@
"dev": true
},
"get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
"integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
"requires": {
"pump": "^3.0.0"
},
@@ -2163,16 +2146,15 @@
}
},
"google-auth-library": {
"version": "5.10.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz",
"integrity": "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg==",
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.9.2.tgz",
"integrity": "sha512-rBE1YTOZ3/Hu6Mojkr+UUmbdc/F28hyMGYEGxjyfVA9ZFmq12oqS3AeftX4h9XpdVIcxPooSo8hECYGT6B9XqQ==",
"requires": {
"arrify": "^2.0.0",
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"fast-text-encoding": "^1.0.0",
"gaxios": "^2.1.0",
"gcp-metadata": "^3.4.0",
"gcp-metadata": "^3.3.0",
"gtoken": "^4.1.0",
"jws": "^4.0.0",
"lru-cache": "^5.0.0"
@@ -2335,13 +2317,22 @@
}
},
"http-proxy-agent": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
"integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.0.tgz",
"integrity": "sha512-GX0FA6+IcDf4Oxc/FBWgYj4zKgo/DnZrksaG9jyuQLExs6xlX+uI5lcA8ymM3JaZTRrF/4s2UX19wJolyo7OBA==",
"requires": {
"@tootallnate/once": "1",
"agent-base": "6",
"debug": "4"
},
"dependencies": {
"agent-base": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz",
"integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==",
"requires": {
"debug": "4"
}
}
}
},
"http-signature": {
@@ -2376,11 +2367,11 @@
"integrity": "sha1-QzX/2CzZaWaKOUZckprGHWOTYn8="
},
"https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz",
"integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==",
"requires": {
"agent-base": "6",
"agent-base": "5",
"debug": "4"
}
},
@@ -2447,9 +2438,9 @@
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
},
"ipaddr.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.0.tgz",
"integrity": "sha512-S54H9mIj0rbxRIyrDMEuuER86LdlgUg9FSeZ8duQb6CUG2iRrA36MYVQBSprTF/ZeAwvyQ5mDGuNvIPM0BIl3w=="
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
"integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA=="
},
"is-arguments": {
"version": "1.0.4",
@@ -2618,12 +2609,6 @@
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"jsonfile": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
"integrity": "sha1-6l7+QLg2kLmGZ2FKc5L8YOhCwN0=",
"dev": true
},
"jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
@@ -2675,26 +2660,42 @@
}
},
"ldap-filter": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
"integrity": "sha1-KxTGiiqdQQTb28kQocqF/Riel5c=",
"version": "0.2.2",
"resolved": false,
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
"requires": {
"assert-plus": "^1.0.0"
"assert-plus": "0.1.5"
},
"dependencies": {
"assert-plus": {
"version": "0.1.5",
"resolved": false,
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
}
}
},
"ldapjs": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.2.0.tgz",
"integrity": "sha512-9+ekbj97nxRYQMRgEm/HYFhFLWSRKah2PnReUfhfM5f62XBeUSFolB+AQ2Jij5tqowpksTnrdNCZLP6C+V3uag==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz",
"integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=",
"requires": {
"abstract-logging": "^2.0.0",
"asn1": "^0.2.4",
"asn1": "0.2.3",
"assert-plus": "^1.0.0",
"backoff": "^2.5.0",
"ldap-filter": "^0.3.3",
"bunyan": "^1.8.3",
"dashdash": "^1.14.0",
"dtrace-provider": "~0.8",
"ldap-filter": "0.2.2",
"once": "^1.4.0",
"vasync": "^2.2.0",
"vasync": "^1.6.4",
"verror": "^1.8.1"
},
"dependencies": {
"asn1": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
}
}
},
"load-json-file": {
@@ -2728,9 +2729,9 @@
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash.chunk": {
"version": "4.2.0",
@@ -2886,9 +2887,9 @@
}
},
"mocha": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.3.tgz",
"integrity": "sha512-0R/3FvjIGH3eEuG17ccFPk117XL2rWxatr81a57D+r/x2uTYZRbdZ4oVidEUMh2W2TJDa7MdAb12Lm2/qrKajg==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.2.tgz",
"integrity": "sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==",
"dev": true,
"requires": {
"ansi-colors": "3.2.3",
@@ -2903,7 +2904,7 @@
"js-yaml": "3.13.1",
"log-symbols": "2.2.0",
"minimatch": "3.0.4",
"mkdirp": "0.5.4",
"mkdirp": "0.5.1",
"ms": "2.1.1",
"node-environment-flags": "1.0.5",
"object.assign": "4.1.0",
@@ -2911,8 +2912,8 @@
"supports-color": "6.0.0",
"which": "1.3.1",
"wide-align": "1.1.3",
"yargs": "13.3.2",
"yargs-parser": "13.1.2",
"yargs": "13.3.0",
"yargs-parser": "13.1.1",
"yargs-unparser": "1.6.0"
},
"dependencies": {
@@ -2949,21 +2950,6 @@
"esprima": "^4.0.0"
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"mkdirp": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz",
"integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
@@ -2978,34 +2964,6 @@
"requires": {
"isexe": "^2.0.0"
}
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
@@ -3018,6 +2976,42 @@
"underscore": "1.8.3"
},
"dependencies": {
"fs-extra": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.6.4.tgz",
"integrity": "sha1-9G8MdbeEH40gCzNIzU1pHVoJnRU=",
"dev": true,
"requires": {
"jsonfile": "~1.0.1",
"mkdirp": "0.3.x",
"ncp": "~0.4.2",
"rimraf": "~2.2.0"
}
},
"jsonfile": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
"integrity": "sha1-6l7+QLg2kLmGZ2FKc5L8YOhCwN0=",
"dev": true
},
"mkdirp": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=",
"dev": true
},
"ncp": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz",
"integrity": "sha1-q8xsvT7C7Spyn/bnwfqPAXhKhXQ=",
"dev": true
},
"rimraf": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=",
"dev": true
},
"underscore": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
@@ -3027,9 +3021,9 @@
}
},
"moment": {
"version": "2.29.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.0.tgz",
"integrity": "sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA=="
"version": "2.26.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz",
"integrity": "sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw=="
},
"moment-timezone": {
"version": "0.5.31",
@@ -3082,37 +3076,14 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"multiparty": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz",
"integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.1.tgz",
"integrity": "sha512-AvESCnNoQlZiOfP9R4mxN8M9csy2L16EIbWIkt3l4FuGti9kXBS8QVzlfyg4HEnarJhrzZilgNFlZtqmoiAIIA==",
"requires": {
"http-errors": "~1.8.0",
"safe-buffer": "5.2.1",
"fd-slicer": "1.1.0",
"http-errors": "~1.7.0",
"safe-buffer": "5.1.2",
"uid-safe": "2.1.5"
},
"dependencies": {
"http-errors": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
"integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
}
}
},
"mute-stream": {
@@ -3120,6 +3091,47 @@
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
},
"mv": {
"version": "2.1.1",
"resolved": false,
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"optional": true,
"requires": {
"mkdirp": "~0.5.1",
"ncp": "~2.0.0",
"rimraf": "~2.4.0"
},
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": false,
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"optional": true,
"requires": {
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "2 || 3",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"ncp": {
"version": "2.0.0",
"resolved": false,
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
"optional": true
},
"rimraf": {
"version": "2.4.5",
"resolved": false,
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"optional": true,
"requires": {
"glob": "^6.0.1"
}
}
}
},
"mysql": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
@@ -3141,8 +3153,7 @@
"nan": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
"dev": true
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
},
"ncp": {
"version": "1.0.1",
@@ -3217,9 +3228,9 @@
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
},
"node-forge": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.2.tgz",
"integrity": "sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw=="
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz",
"integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ=="
},
"node-fs": {
"version": "0.1.7",
@@ -3366,9 +3377,9 @@
}
},
"nodemailer": {
"version": "6.4.11",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.11.tgz",
"integrity": "sha512-BVZBDi+aJV4O38rxsUh164Dk1NCqgh6Cm0rQSb9SK/DHGll/DrCMnycVDD7msJgZCnmVa8ASo8EZzR7jsgTukQ=="
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.6.tgz",
"integrity": "sha512-/kJ+FYVEm2HuUlw87hjSqTss+GU35D4giOpdSfGp7DO+5h6RlJj7R94YaYHOkoxu1CSaM0d3WRBtCzwXrY6MKA=="
},
"nodemailer-fetch": {
"version": "1.6.0",
@@ -3607,6 +3618,11 @@
"error-ex": "^1.2.0"
}
},
"parse-links": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/parse-links/-/parse-links-0.1.0.tgz",
"integrity": "sha1-afpighugBBX+c2MyNVIeRUe36CE="
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -3662,6 +3678,11 @@
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
"dev": true
},
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -3704,13 +3725,13 @@
},
"precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"resolved": false,
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"pretty-bytes": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.4.1.tgz",
"integrity": "sha512-s1Iam6Gwz3JI5Hweaz4GoCD1WUNUIyzePFy5+Js2hjwGVt2Z79wNN+ZKOZ2vB6C+Xs6njyB84Z1IthQg8d9LxA=="
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz",
"integrity": "sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg=="
},
"process-nextick-args": {
"version": "2.0.1",
@@ -3763,13 +3784,6 @@
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.0"
},
"dependencies": {
"ipaddr.js": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
"integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA=="
}
}
},
"proxy-middleware": {
@@ -4095,6 +4109,12 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-json-stringify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -4552,21 +4572,21 @@
"integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls="
},
"superagent": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-5.3.1.tgz",
"integrity": "sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-5.2.2.tgz",
"integrity": "sha512-pMWBUnIllK4ZTw7p/UaobiQPwAO5w/1NRRTDpV0FTVNmECztsxKspj3ZWEordVEaqpZtmOQJJna4yTLyC/q7PQ==",
"requires": {
"component-emitter": "^1.3.0",
"cookiejar": "^2.1.2",
"debug": "^4.1.1",
"fast-safe-stringify": "^2.0.7",
"form-data": "^3.0.0",
"formidable": "^1.2.2",
"formidable": "^1.2.1",
"methods": "^1.1.2",
"mime": "^2.4.6",
"qs": "^6.9.4",
"readable-stream": "^3.6.0",
"semver": "^7.3.2"
"mime": "^2.4.4",
"qs": "^6.9.1",
"readable-stream": "^3.4.0",
"semver": "^6.3.0"
},
"dependencies": {
"form-data": {
@@ -4593,11 +4613,6 @@
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
}
}
},
@@ -4673,11 +4688,11 @@
}
},
"tar-stream": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz",
"integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz",
"integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==",
"requires": {
"bl": "^4.0.3",
"bl": "^4.0.1",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
@@ -4685,9 +4700,9 @@
},
"dependencies": {
"bl": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz",
"integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz",
"integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==",
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
@@ -4716,22 +4731,15 @@
}
},
"teeny-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.3.tgz",
"integrity": "sha512-TZG/dfd2r6yeji19es1cUIwAlVD8y+/svB1kAC2Y0bjEyysrfbO8EZvJBRwIE6WkwmUoB7uvWLwTIhJbMXZ1Dw==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz",
"integrity": "sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==",
"requires": {
"http-proxy-agent": "^4.0.0",
"https-proxy-agent": "^5.0.0",
"https-proxy-agent": "^4.0.0",
"node-fetch": "^2.2.0",
"stream-events": "^1.0.5",
"uuid": "^7.0.0"
},
"dependencies": {
"uuid": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
"integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="
}
"uuid": "^3.3.2"
}
},
"through": {
@@ -4880,9 +4888,9 @@
}
},
"underscore": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz",
"integrity": "sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw=="
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz",
"integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg=="
},
"unique-string": {
"version": "1.0.0",
@@ -4994,11 +5002,21 @@
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"vasync": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.0.tgz",
"integrity": "sha1-z951GGChWCLbOxMrxZsRakra8Bs=",
"version": "1.6.4",
"resolved": false,
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
"requires": {
"verror": "1.10.0"
"verror": "1.6.0"
},
"dependencies": {
"verror": {
"version": "1.6.0",
"resolved": false,
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
"requires": {
"extsprintf": "1.2.0"
}
}
}
},
"verror": {
@@ -5127,9 +5145,9 @@
}
},
"ws": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA=="
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz",
"integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w=="
},
"xdg-basedir": {
"version": "3.0.0",
+18 -18
View File
@@ -14,41 +14,41 @@
"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.759.0",
"aws-sdk": "^2.685.0",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^5.6.0",
"cloudron-manifestformat": "^5.5.0",
"connect": "^3.7.0",
"connect-lastmile": "^2.0.0",
"connect-timeout": "^1.9.0",
"cookie-session": "^1.4.0",
"cron": "^1.8.2",
"db-migrate": "^0.11.11",
"db-migrate-mysql": "^2.1.1",
"debug": "^4.2.0",
"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",
"ldapjs": "^2.2.0",
"lodash": "^4.17.20",
"ldapjs": "^1.0.2",
"lodash": "^4.17.15",
"lodash.chunk": "^4.2.0",
"mime": "^2.4.6",
"moment": "^2.29.0",
"moment": "^2.26.0",
"moment-timezone": "^0.5.31",
"morgan": "^1.10.0",
"multiparty": "^4.2.2",
"multiparty": "^4.2.1",
"mysql": "^2.18.1",
"nodemailer": "^6.4.11",
"nodemailer": "^6.4.6",
"nodemailer-smtp-transport": "^2.7.4",
"once": "^1.4.0",
"pretty-bytes": "^5.4.1",
"parse-links": "^0.1.0",
"pretty-bytes": "^5.3.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.4.4",
@@ -61,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.1.4",
"tar-stream": "^2.1.2",
"tldjs": "^2.3.1",
"underscore": "^1.11.0",
"underscore": "^1.10.2",
"uuid": "^3.4.0",
"validator": "^11.0.0",
"ws": "^7.3.1",
"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",
+1 -1
View File
@@ -53,7 +53,7 @@ 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)
+3 -3
View File
@@ -37,7 +37,7 @@ 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}"
@@ -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
+6 -11
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,7 +57,7 @@ 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)
readonly nginx_version=$(nginx -v)
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-1~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
@@ -66,11 +66,6 @@ if [[ "${nginx_version}" != *"1.18."* ]]; then
rm /tmp/nginx.deb
fi
if ! which ipset; then
echo "==> installer: installing ipset"
apt install -y ipset
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v10.18.1" ]]; then
mkdir -p /usr/local/node-10.18.1
@@ -129,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
+3 -16
View File
@@ -44,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"
@@ -57,7 +57,6 @@ 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
@@ -101,13 +100,11 @@ unbound-anchor -a /var/lib/unbound/root.key
echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
systemctl disable cloudron.target || true
rm -f /etc/systemd/system/cloudron.target
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
systemctl daemon-reload
systemctl enable unbound
systemctl enable cloudron-syslog
systemctl enable box
systemctl enable cloudron.target
systemctl enable cloudron-firewall
# update firewall rules
@@ -130,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"
@@ -193,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
@@ -240,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
+10 -20
View File
@@ -6,25 +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
# 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
@@ -66,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
@@ -81,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
+3 -3
View File
@@ -14,8 +14,8 @@ if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
printf "\t\t\t-------------------\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://docs.cloudron.io/ \n"
printf "Cloudron setup - https://docs.cloudron.io/installation/#setup \n"
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"
@@ -23,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/"
+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
+3 -4
View File
@@ -1,20 +1,19 @@
[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
; 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:ldap" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well
KillMode=control-group
+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
@@ -4,4 +4,4 @@ set -eu -o pipefail
echo "Stopping cloudron"
systemctl stop box
systemctl stop cloudron.target
+93 -174
View File
@@ -1,30 +1,29 @@
'use strict';
exports = module.exports = {
getServices,
getService,
configureService,
getServiceLogs,
restartService,
rebuildService,
getServices: getServices,
getService: getService,
configureService: configureService,
getServiceLogs: getServiceLogs,
restartService: restartService,
startAppServices,
stopAppServices,
startServices,
updateServiceConfig,
startServices: startServices,
updateServiceConfig: updateServiceConfig,
setupAddons,
teardownAddons,
backupAddons,
restoreAddons,
clearAddons,
setupAddons: setupAddons,
teardownAddons: teardownAddons,
backupAddons: backupAddons,
restoreAddons: restoreAddons,
clearAddons: clearAddons,
getEnvironment,
getMountsSync,
getContainerNamesSync,
getEnvironment: getEnvironment,
getMountsSync: getMountsSync,
getContainerNamesSync: getContainerNamesSync,
getContainerDetails,
getContainerDetails: getContainerDetails,
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
SERVICE_STATUS_ACTIVE: 'active',
@@ -278,7 +277,7 @@ function restartContainer(name, callback) {
docker.restartContainer(name, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) {
callback(null); // callback early since rebuilding takes long
return rebuildService(name, function (error) { if (error) debug(`restartContainer: Unable to rebuild service ${name}`, error); });
return rebuildService(name, function (error) { if (error) console.error(`Unable to rebuild service ${name}`, error); });
}
if (error) return callback(error);
@@ -734,10 +733,10 @@ function importDatabase(addon, callback) {
debug(`importDatabase: Importing ${addon}`);
appdb.getAll(function (error, allApps) {
appdb.getAll(function (error, apps) {
if (error) return callback(error);
async.eachSeries(allApps, function iterator (app, iteratorCallback) {
async.eachSeries(apps, function iterator (app, iteratorCallback) {
if (!(addon in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
debug(`importDatabase: Importing addon ${addon} of app ${app.id}`);
@@ -750,57 +749,14 @@ function importDatabase(addon, callback) {
// not clear, if repair workflow should be part of addon or per-app
appdb.update(app.id, { installationState: apps.ISTATE_ERROR, error: { message: error.message } }, iteratorCallback);
});
}, function (error) {
safe.fs.unlinkSync(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`)); // clean up for future migrations
callback(error);
});
});
}
function exportDatabase(addon, callback) {
assert.strictEqual(typeof addon, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`exportDatabase: Exporting ${addon}`);
if (fs.existsSync(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`))) {
debug(`exportDatabase: Already exported addon ${addon} in previous run`);
return callback(null);
}
appdb.getAll(function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function iterator (app, iteratorCallback) {
if (!app.manifest.addons || !(addon in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
debug(`exportDatabase: Exporting addon ${addon} of app ${app.id}`);
ADDONS[addon].backup(app, app.manifest.addons[addon], function (error) {
if (error) {
debug(`exportDatabase: Error exporting ${addon} of app ${app.id}.`, error);
return iteratorCallback(error);
}
iteratorCallback();
});
}, function (error) {
if (error) return callback(error);
async.series([
(done) => fs.writeFile(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`), '', 'utf8', done),
// note: after this point, we are restart safe. it's ok if the box code crashes at this point
(done) => shell.exec(`exportDatabase - remove${addon}`, `docker rm -f ${addon}`, done), // what if db writes something when quitting ...
(done) => shell.sudo(`exportDatabase - removeAddonDir${addon}`, [ RMADDONDIR_CMD, addon ], {}, done) // ready to start afresh
], callback);
});
}, callback);
});
}
function updateServiceConfig(platformConfig, callback) {
assert.strictEqual(typeof platformConfig, 'object');
assert.strictEqual(typeof callback, 'function');
callback = callback || NOOP_CALLBACK;
debug('updateServiceConfig: %j', platformConfig);
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb', 'graphite' ], function iterator(serviceName, iteratorCallback) {
const containerConfig = platformConfig[serviceName];
@@ -814,11 +770,7 @@ function updateServiceConfig(platformConfig, callback) {
}
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${serviceName}`.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(`updateServiceConfig(${serviceName})`, '/usr/bin/docker', args, { }, retryCallback);
}, iteratorCallback);
shell.spawn(`update${serviceName}`, '/usr/bin/docker', args, { }, iteratorCallback);
}, callback);
}
@@ -863,7 +815,7 @@ function startServices(existingInfra, callback) {
} else {
assert.strictEqual(typeof existingInfra.images, 'object');
if (infra.images.turn.tag !== existingInfra.images.turn.tag) startFuncs.push(startTurn.bind(null, existingInfra));
if (!existingInfra.images.turn || infra.images.turn.tag !== existingInfra.images.turn.tag) startFuncs.push(startTurn.bind(null, existingInfra));
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql.bind(null, existingInfra));
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql.bind(null, existingInfra));
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb.bind(null, existingInfra));
@@ -980,7 +932,7 @@ function setupTurn(app, options, callback) {
assert.strictEqual(typeof callback, 'function');
var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8');
if (!turnSecret) debug('setupTurn: no turn secret set. Will leave emtpy, but this is a problem!');
if (!turnSecret) console.error('No turn secret set. Will leave emtpy, but this is a problem!');
const env = [
{ name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() },
@@ -1029,7 +981,6 @@ function setupEmail(app, options, callback) {
{ name: `${envPrefix}MAIL_SIEVE_PORT`, value: '4190' },
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain },
{ name: `${envPrefix}MAIL_DOMAINS`, value: mailInDomains },
{ name: 'CLOUDRON_MAIL_SERVER_HOST', value: settings.mailFqdn() },
{ name: `${envPrefix}LDAP_MAILBOXES_BASE_DN`, value: 'ou=mailboxes,dc=cloudron' }
];
@@ -1060,7 +1011,6 @@ function setupLdap(app, options, callback) {
var env = [
{ name: `${envPrefix}LDAP_SERVER`, value: '172.18.0.1' },
{ name: 'CLOUDRON_LDAP_HOST', value: '172.18.0.1' }, // to keep things in sync with the database _HOST vars
{ name: `${envPrefix}LDAP_PORT`, value: '' + constants.LDAP_PORT },
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + constants.LDAP_PORT },
{ name: `${envPrefix}LDAP_USERS_BASE_DN`, value: 'ou=users,dc=cloudron' },
@@ -1176,16 +1126,16 @@ function startMysql(existingInfra, callback) {
const dataDir = paths.PLATFORM_DATA_DIR;
const rootPassword = hat(8 * 128);
const cloudronToken = hat(8 * 128);
const memoryLimit = 4 * 256;
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mysql.tag, tag);
if (upgrading) debug('startMysql: mysql will be upgraded');
const upgradeFunc = upgrading ? exportDatabase.bind(null, 'mysql') : (next) => next();
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startMysql', [ RMADDONDIR_CMD, 'mysql' ], {}) : (next) => next();
upgradeFunc(function (error) {
if (error) return callback(error);
// memory options are applied dynamically. import requires all the memory we can get
const cmd = `docker run --restart=always -d --name="mysql" \
--hostname mysql \
--net cloudron \
@@ -1194,6 +1144,8 @@ function startMysql(existingInfra, callback) {
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mysql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MYSQL_TOKEN=${cloudronToken} \
@@ -1203,11 +1155,7 @@ function startMysql(existingInfra, callback) {
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
async.series([
shell.exec.bind(null, 'stopMysql', 'docker stop mysql || true'),
shell.exec.bind(null, 'removeMysql', 'docker rm -f mysql || true'),
shell.exec.bind(null, 'startMysql', cmd)
], function (error) {
shell.exec('startMysql', cmd, function (error) {
if (error) return callback(error);
waitForContainer('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
@@ -1395,16 +1343,16 @@ function startPostgresql(existingInfra, callback) {
const dataDir = paths.PLATFORM_DATA_DIR;
const rootPassword = hat(8 * 128);
const cloudronToken = hat(8 * 128);
const memoryLimit = 4 * 256;
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.postgresql.tag, tag);
if (upgrading) debug('startPostgresql: postgresql will be upgraded');
const upgradeFunc = upgrading ? exportDatabase.bind(null, 'postgresql') : (next) => next();
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startPostgresql', [ RMADDONDIR_CMD, 'postgresql' ], {}) : (next) => next();
upgradeFunc(function (error) {
if (error) return callback(error);
// memory options are applied dynamically. import requires all the memory we can get
const cmd = `docker run --restart=always -d --name="postgresql" \
--hostname postgresql \
--net cloudron \
@@ -1413,6 +1361,8 @@ function startPostgresql(existingInfra, callback) {
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=postgresql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_POSTGRESQL_ROOT_PASSWORD="${rootPassword}" \
@@ -1421,11 +1371,7 @@ function startPostgresql(existingInfra, callback) {
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
async.series([
shell.exec.bind(null, 'stopPostgresql', 'docker stop postgresql || true'),
shell.exec.bind(null, 'removePostgresql', 'docker rm -f postgresql || true'),
shell.exec.bind(null, 'startPostgresql', cmd)
], function (error) {
shell.exec('startPostgresql', cmd, function (error) {
if (error) return callback(error);
waitForContainer('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
@@ -1581,6 +1527,8 @@ function startTurn(existingInfra, callback) {
const memoryLimit = 256;
const realm = settings.adminFqdn();
if (existingInfra.version === infra.version && existingInfra.images.turn && infra.images.turn.tag === existingInfra.images.turn.tag) return callback();
// this exports 3478/tcp, 5349/tls and 50000-51000/udp
const cmd = `docker run --restart=always -d --name="turn" \
--hostname turn \
@@ -1598,11 +1546,7 @@ function startTurn(existingInfra, callback) {
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
async.series([
shell.exec.bind(null, 'stopTurn', 'docker stop turn || true'),
shell.exec.bind(null, 'removeTurn', 'docker rm -f turn || true'),
shell.exec.bind(null, 'startTurn', cmd)
], callback);
shell.exec('startTurn', cmd, callback);
}
function startMongodb(existingInfra, callback) {
@@ -1613,16 +1557,16 @@ function startMongodb(existingInfra, callback) {
const dataDir = paths.PLATFORM_DATA_DIR;
const rootPassword = hat(8 * 128);
const cloudronToken = hat(8 * 128);
const memoryLimit = 4 * 256;
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mongodb.tag, tag);
if (upgrading) debug('startMongodb: mongodb will be upgraded');
const upgradeFunc = upgrading ? exportDatabase.bind(null, 'mongodb') : (next) => next();
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startMongodb', [ RMADDONDIR_CMD, 'mongodb' ], {}) : (next) => next();
upgradeFunc(function (error) {
if (error) return callback(error);
// memory options are applied dynamically. import requires all the memory we can get
const cmd = `docker run --restart=always -d --name="mongodb" \
--hostname mongodb \
--net cloudron \
@@ -1631,6 +1575,8 @@ function startMongodb(existingInfra, callback) {
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mongodb \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MONGODB_ROOT_PASSWORD="${rootPassword}" \
@@ -1639,11 +1585,7 @@ function startMongodb(existingInfra, callback) {
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
async.series([
shell.exec.bind(null, 'stopMongodb', 'docker stop mongodb || true'),
shell.exec.bind(null, 'removeMongodb', 'docker rm -f mongodb || true'),
shell.exec.bind(null, 'startMongodb', cmd)
], function (error) {
shell.exec('startMongodb', cmd, function (error) {
if (error) return callback(error);
waitForContainer('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
@@ -1666,41 +1608,37 @@ function setupMongoDb(app, options, callback) {
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) {
database = database || hat(8 * 8); // 16 bytes. keep this short, so as to not overflow the 127 byte index length in MongoDB < 4.4
const data = {
database: app.id,
username: app.id,
password: error ? hat(4 * 128) : existingPassword,
oplog: !!options.oplog
};
const data = {
database: database,
username: app.id,
password: error ? hat(4 * 128) : existingPassword,
oplog: !!options.oplog
};
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up mongodb: ${error.message}`));
if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up mongodb: ${error.message}`));
if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/${data.database}` },
{ name: `${envPrefix}MONGODB_USERNAME`, value : data.username },
{ name: `${envPrefix}MONGODB_PASSWORD`, value: data.password },
{ name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' },
{ name: `${envPrefix}MONGODB_PORT`, value : '27017' },
{ name: `${envPrefix}MONGODB_DATABASE`, value : data.database }
];
var env = [
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/${data.database}` },
{ name: `${envPrefix}MONGODB_USERNAME`, value : data.username },
{ name: `${envPrefix}MONGODB_PASSWORD`, value: data.password },
{ name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' },
{ name: `${envPrefix}MONGODB_PORT`, value : '27017' },
{ name: `${envPrefix}MONGODB_DATABASE`, value : data.database }
];
if (options.oplog) {
env.push({ name: `${envPrefix}MONGODB_OPLOG_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/local?authSource=${data.database}` });
}
if (options.oplog) {
env.push({ name: `${envPrefix}MONGODB_OPLOG_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/local?authSource=${data.database}` });
}
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
});
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
});
});
});
@@ -1711,18 +1649,16 @@ function clearMongodb(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Clearing mongodb');
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing mongodb: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing mongodb: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
callback();
});
callback();
});
});
}
@@ -1732,19 +1668,16 @@ function teardownMongoDb(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Tearing down mongodb');
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null);
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/databases/${app.id}?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
appdb.unsetAddonConfig(app.id, 'mongodb', callback);
});
appdb.unsetAddonConfig(app.id, 'mongodb', callback);
});
});
}
@@ -1759,12 +1692,8 @@ function backupMongoDb(app, options, callback) {
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`;
pipeRequestToFile(url, dumpPath('mongodb', app.id), callback);
});
const url = `https://${result.ip}:3000/databases/${app.id}/backup?access_token=${result.token}`;
pipeRequestToFile(url, dumpPath('mongodb', app.id), callback);
});
}
@@ -1780,21 +1709,17 @@ function restoreMongoDb(app, options, callback) {
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) {
if (error) return callback(error);
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
readStream.on('error', (error) => callback(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring mongodb: ${error.message}`)));
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
readStream.on('error', (error) => callback(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring mongodb: ${error.message}`)));
const restoreReq = request.post(`https://${result.ip}:3000/databases/${app.id}/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
const restoreReq = request.post(`https://${result.ip}:3000/databases/${database}/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
callback(null);
});
readStream.pipe(restoreReq);
callback(null);
});
readStream.pipe(restoreReq);
});
}
@@ -1811,19 +1736,13 @@ function startRedis(existingInfra, callback) {
async.eachSeries(allApps, function iterator (app, iteratorCallback) {
if (!('redis' in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
const redisName = 'redis-' + app.id;
async.series([
shell.exec.bind(null, 'stopRedis', `docker stop ${redisName} || true`), // redis will backup as part of signal handling
shell.exec.bind(null, 'removeRedis', `docker rm -f ${redisName} || true`),
setupRedis.bind(null, app, app.manifest.addons.redis) // starts the container
], iteratorCallback);
setupRedis(app, app.manifest.addons.redis, iteratorCallback);
}, function (error) {
if (error) return callback(error);
if (!upgrading) return callback();
importDatabase('redis', callback);
importDatabase('redis', callback); // setupRedis currently starts the app container
});
});
}
+7 -7
View File
@@ -423,14 +423,14 @@ 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 ]});
queries.push({ query: 'UPDATE subdomains SET subdomain = ?, domain = ? WHERE appId = ? AND type = ?', args: [ app.location, app.domain, id, 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 ('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 = [ ];
+9 -2
View File
@@ -79,8 +79,15 @@ 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);
+8 -25
View File
@@ -41,7 +41,6 @@ exports = module.exports = {
backup: backup,
listBackups: listBackups,
getLocalLogfilePaths: getLocalLogfilePaths,
getLogs: getLogs,
start: start,
@@ -1366,19 +1365,6 @@ function update(app, data, auditSource, callback) {
});
}
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');
@@ -1398,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';
@@ -2033,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);
});
+15 -46
View File
@@ -32,13 +32,11 @@ var apps = require('./apps.js'),
constants = require('./constants.js'),
debug = require('debug')('box:appstore'),
eventlog = require('./eventlog.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
support = require('./support.js'),
util = require('util');
// These are the default options and will be adjusted once a subscription state is obtained
@@ -351,8 +349,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);
});
}
@@ -363,8 +361,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);
});
}
@@ -408,55 +406,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) console.error('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(20 * 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 for 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!` });
});
});
});
+11 -15
View File
@@ -17,6 +17,8 @@ exports = module.exports = {
_waitForDnsPropagation: waitForDnsPropagation
};
require('supererror')({ splatchError: true });
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
@@ -738,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);
});
}
@@ -784,8 +785,7 @@ 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);
});
}
@@ -852,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) debug('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)
@@ -868,10 +868,10 @@ 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' }),
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),
@@ -978,18 +978,16 @@ function uninstall(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 30, message: 'Teardown 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' }),
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' }),
@@ -1045,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));
+1 -8
View File
@@ -12,7 +12,6 @@ let assert = require('assert'),
safe = require('safetydance'),
path = require('path'),
paths = require('./paths.js'),
sftp = require('./sftp.js'),
tasks = require('./tasks.js');
let gActiveTasks = { }; // indexed by app id
@@ -69,17 +68,11 @@ function scheduleTask(appId, taskId, callback) {
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
// TODO: set memory limit for app backup task
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */, nice: 15 }, 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
sftp.rebuild(function (error) {
if (error) console.error('Unable to rebuild sftp:', error);
});
});
}
+63 -61
View File
@@ -9,6 +9,7 @@ exports = module.exports = {
get: get,
startBackupTask: startBackupTask,
ensureBackup: ensureBackup,
restore: restore,
@@ -56,7 +57,6 @@ var addons = require('./addons.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'),
@@ -143,8 +143,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 +380,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 +515,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);
@@ -904,13 +901,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();
});
}
@@ -920,6 +913,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);
@@ -935,22 +930,19 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
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');
@@ -981,7 +973,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);
@@ -993,10 +985,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');
@@ -1006,7 +997,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);
});
});
}
@@ -1030,7 +1021,6 @@ 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))) {
@@ -1040,8 +1030,6 @@ function snapshotApp(app, progressCallback, callback) {
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);
});
}
@@ -1054,8 +1042,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
@@ -1088,7 +1074,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);
});
@@ -1102,6 +1088,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);
@@ -1120,12 +1108,10 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
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 +1161,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');
@@ -1197,14 +1182,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
});
@@ -1216,7 +1200,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);
});
});
}
@@ -1225,30 +1209,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');
@@ -1286,13 +1289,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;
}
}
@@ -1526,9 +1528,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:
+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'));
}
+61 -102
View File
@@ -1,24 +1,24 @@
'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,
};
var addons = require('./addons.js'),
@@ -46,7 +46,6 @@ var addons = require('./addons.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');
@@ -67,7 +66,7 @@ function uninitialize(callback) {
async.series([
cron.stopJobs,
platform.stopAllTasks
platform.stop
], callback);
}
@@ -79,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);
}
@@ -113,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);
});
}
@@ -185,7 +150,7 @@ function getConfig(callback) {
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);
});
@@ -203,11 +168,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');
@@ -298,7 +276,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);
@@ -320,12 +298,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 });
@@ -337,24 +315,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);
addons.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');
@@ -368,34 +358,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);
});
});
});
}
+63 -26
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,7 +16,8 @@ exports = module.exports = {
handleSettingsChanged,
DEFAULT_AUTOUPDATE_PATTERN,
DEFAULT_BOX_AUTOUPDATE_PATTERN,
DEFAULT_APP_AUTOUPDATE_PATTERN
};
var appHealthMonitor = require('./apphealthmonitor.js'),
@@ -36,9 +40,11 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
updateChecker = require('./updatechecker.js');
var gJobs = {
autoUpdater: null,
appAutoUpdater: null,
boxAutoUpdater: null,
appUpdateChecker: null,
backup: null,
updateChecker: null,
boxUpdateChecker: null,
systemChecks: null,
diskSpaceChecker: null,
certificateRenew: null,
@@ -64,9 +70,9 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function startJobs(callback) {
assert.strictEqual(typeof callback, 'function');
const randomTick = Math.floor(60*Math.random());
const randomMinute = 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
});
@@ -77,10 +83,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
});
@@ -131,7 +142,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();
@@ -146,7 +158,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([
@@ -163,47 +176,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
});
+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);
});
});
}
+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);
});
});
+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');
+20 -32
View File
@@ -17,6 +17,7 @@ exports = module.exports = {
stopContainerByName: stopContainer,
stopContainers: stopContainers,
deleteContainer: deleteContainer,
deleteContainerByName: deleteContainer,
deleteImage: deleteImage,
deleteContainers: deleteContainers,
createSubcontainer: createSubcontainer,
@@ -199,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_';
@@ -255,9 +257,15 @@ function createSubcontainer(app, name, cmd, options, callback) {
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(error);
let containerOptions = {
// 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),
@@ -294,34 +302,21 @@ function createSubcontainer(app, name, cmd, options, callback) {
},
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: []
}
};
// 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 = {
},
NetworkingConfig: {
EndpointsConfig: {
cloudron: {
Aliases: [ name ] // adds hostname entry with container name
Aliases: [ name ] // this allows sub-containers reach app containers by name
}
}
};
} else {
containerOptions.HostConfig.NetworkMode = `container:${app.containerId}`;
}
}
};
var capabilities = manifest.capabilities || [];
@@ -330,16 +325,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
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
if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) {
containerOptions.HostConfig.Devices = [
{ PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' }
];
}
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);
@@ -407,7 +395,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');
@@ -519,7 +507,7 @@ 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);
+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 });
});
+59 -6
View File
@@ -26,10 +26,13 @@ module.exports = exports = {
parentDomain: parentDomain,
checkDnsRecords: 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');
@@ -89,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)
@@ -128,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;
}
@@ -139,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) {
@@ -302,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);
@@ -331,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) {
@@ -439,7 +461,8 @@ 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;
}
@@ -451,3 +474,33 @@ function makeWildcard(hostname) {
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);
});
});
});
}
-1
View File
@@ -39,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',
+9 -15
View File
@@ -115,9 +115,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));
@@ -310,7 +307,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));
}
@@ -399,7 +396,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 +461,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 +503,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 +514,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 ];
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 +536,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();
});
});
+1 -6
View File
@@ -5,7 +5,6 @@ exports = module.exports = {
};
var assert = require('assert'),
async = require('async'),
infra = require('./infra_version.js'),
paths = require('./paths.js'),
shell = require('./shell.js');
@@ -38,9 +37,5 @@ function startGraphite(existingInfra, 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 -1
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(',') : [ ]; });
+6 -6
View File
@@ -6,7 +6,7 @@
exports = module.exports = {
// a version change recreates all containers with latest docker config
'version': '48.17.1',
'version': '48.17.0',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:2.0.0@sha256:f9fea80513aa7c92fe2e7bf3978b54c8ac5222f47a9a32a7f8833edf0eb5a4f4' }
@@ -16,12 +16,12 @@ exports = module.exports = {
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
'images': {
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.1.0@sha256:e1dd22aa6eef5beb7339834b200a8bb787ffc2264ce11139857a054108fefb4f' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.2@sha256:dd624870c7f8ba9b2759f93ce740d1e092a1ac4b2d6af5007a01b30ad6b316d0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:3.2.1@sha256:ca45ba2c8356fd1ec5ec996a4e8ce1e9df6711b36c358ca19f6ab4bdc476695e' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:3.0.0@sha256:59e50b1f55e433ffdf6d678f8c658812b4119f631db8325572a52ee40d3bc562' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.1@sha256:c1145d43c8a912fe6f5a5629a4052454a4aa6f23391c1efbffeec9d12d72a256' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.3.0@sha256:4112cc31a09b465bdfbd715fedab4bc86898246b4615d789dfb1d2cb728e3872' },
'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:2.10.0@sha256:3aff92bfc85d6ca3cc6fc381c8a89625d2af95cc55ed2db692ef4e483e600372' },
'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:2.0.2@sha256:cbd604eaa970c99ba5c4c2e7984929668e05de824172f880e8c576b2fb7c976d' }
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:2.0.1@sha256:1770cb4c6ba2d324f6ff0d7fd8daeb737064386fc86b8cef425ef48334a67b91' }
}
};
+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');
+4 -4
View File
@@ -577,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,
@@ -645,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);
+28 -116
View File
@@ -4,10 +4,6 @@ exports = module.exports = {
getStatus,
checkConfiguration,
getLocation,
setLocation, // triggers the change task
changeLocation, // does the actual changing
getDomains,
getDomain,
@@ -15,6 +11,7 @@ exports = module.exports = {
onDomainAdded,
onDomainRemoved,
onMailFqdnChanged,
removePrivateFields,
@@ -26,7 +23,6 @@ exports = module.exports = {
setCatchAllAddress,
setMailRelay,
setMailEnabled,
setBanner,
startMail: restartMail,
restartMail,
@@ -37,6 +33,7 @@ exports = module.exports = {
getMailboxCount,
listMailboxes,
removeMailboxes,
getMailbox,
addMailbox,
updateMailboxOwner,
@@ -52,14 +49,12 @@ exports = module.exports = {
removeList,
resolveList,
_removeMailboxes: removeMailboxes,
_readDkimPublicKeySync: readDkimPublicKeySync
};
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'),
@@ -81,14 +76,12 @@ var assert = require('assert'),
shell = require('./shell.js'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.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');
@@ -108,6 +101,7 @@ function checkOutboundPort25(callback) {
var smtpServer = _.sample([
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.1und1.de',
]);
@@ -244,14 +238,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);
@@ -550,7 +544,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
});
@@ -582,18 +576,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',
@@ -603,19 +594,15 @@ 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 */);
});
}
@@ -643,10 +630,7 @@ function configureMail(mailFqdn, mailDomain, 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) {
@@ -904,70 +888,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, 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);
});
});
}
@@ -976,8 +911,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
@@ -1003,7 +936,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;
@@ -1025,20 +958,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));
@@ -1205,25 +1124,18 @@ function updateMailboxOwner(name, domain, userId, auditSource, callback) {
});
}
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 deleteMailFunc = options.deleteMails ? shell.sudo.bind(null, 'removeMailbox', [ REMOVE_MAILBOX, `${name}@${domain}` ], {}) : (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);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
callback();
});
callback(null);
});
}
@@ -1400,7 +1312,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
+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 %>
@@ -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.
+2 -2
View File
@@ -6,8 +6,8 @@ Dear <%= cloudronName %> Admin,
If this message appears repeatedly, give the app more memory.
* To increase an app's memory limit - https://docs.cloudron.io/apps/#memory-limit
* To increase a service's memory limit - https://docs.cloudron.io/troubleshooting/#services
* 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:
+2 -2
View File
@@ -11,7 +11,7 @@ Follow the link to get started.
You are receiving this email because you were invited by <%= invitor.email %>.
<% } %>
Please note that the invite link will expire in 7 days.
Please note that the invite link will expire in 24 hours.
Powered by https://cloudron.io
@@ -39,7 +39,7 @@ Powered by https://cloudron.io
<br/>
Please note that the invite link will expire in 7 days.
Please note that the invite link will expire in 24 hours.
<br/>
Powered by <a href="https://cloudron.io">Cloudron</a>
+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 = ?');
-56
View File
@@ -1,56 +0,0 @@
'use strict';
exports = module.exports = {
getBlocklist,
setBlocklist
};
const assert = require('assert'),
BoxError = require('./boxerror.js'),
ipaddr = require('ipaddr.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
validator = require('validator');
const SET_BLOCKLIST_CMD = path.join(__dirname, 'scripts/setblocklist.sh');
function getBlocklist(callback) {
assert.strictEqual(typeof callback, 'function');
const data = safe.fs.readFileSync(paths.FIREWALL_BLOCKLIST_FILE, 'utf8');
callback(null, data);
}
function setBlocklist(blocklist, auditSource, callback) {
assert.strictEqual(typeof blocklist, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const parsedIp = ipaddr.process(auditSource.ip);
for (const line of blocklist.split('\n')) {
if (!line || line.startsWith('#')) continue;
const rangeOrIP = line.trim();
if (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} is not a valid IP or range`));
if (rangeOrIP.indexOf('/') === -1) {
if (auditSource.ip === rangeOrIP) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`));
} else {
const parsedRange = ipaddr.parseCIDR(rangeOrIP);
if (parsedIp.match(parsedRange)) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`));
}
}
if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
if (!safe.fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, blocklist + '\n', 'utf8')) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
shell.sudo('setBlocklist', [ SET_BLOCKLIST_CMD ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.IPTABLES_ERROR, `Error setting blocklist: ${error.message}`));
callback();
});
}
+30 -40
View File
@@ -6,57 +6,52 @@ map $http_upgrade $connection_upgrade {
# http server
server {
<% if (endpoint === 'ip' || endpoint === 'setup') { -%>
listen 80 default_server;
server_name _;
<% if (hasIPv6) { -%>
listen [::]:80 default_server;
<% } -%>
<% } else { -%>
listen 80;
server_name <%= vhost %>;
<% if (hasIPv6) { -%>
listen [::]:80;
<% } -%>
<% } -%>
listen 80;
server_tokens off; # hide version
<% if (hasIPv6) { -%>
listen [::]:80;
<% } -%>
# acme challenges
<% if (vhost) { -%>
server_name <%= vhost %>;
<% } else { -%>
# IP based access from collectd or initial cloudron setup. TODO: match the IPv6 address
server_name "~^\d+\.\d+\.\d+\.\d+$";
# collectd
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
<% } -%>
# acme challenges (for cert renewal where the vhost config exists)
location /.well-known/acme-challenge/ {
default_type text/plain;
alias /home/yellowtent/platformdata/acme/;
}
# for default server, serve the splash page. for other endpoints, redirect to HTTPS
location / {
<% if ( endpoint === 'admin' || endpoint === 'setup' ) { %>
# redirect everything to HTTPS
return 301 https://$host$request_uri;
<% } else if ( endpoint === 'app' ) { %>
return 301 https://$host$request_uri;
<% } else if ( endpoint === 'redirect' ) { %>
return 301 https://<%= redirectTo %>$request_uri;
<% } else if ( endpoint === 'ip' ) { %>
root <%= sourceDir %>/dashboard/dist;
try_files /splash.html =404;
<% } %>
}
}
# https server
server {
<% if (endpoint === 'ip' || endpoint === 'setup') { -%>
listen 443 ssl http2 default_server;
server_name _;
<% if (hasIPv6) { -%>
listen [::]:443 ssl http2 default_server;
<% } -%>
<% } else { -%>
listen 443 ssl http2;
<% if (vhost) { -%>
server_name <%= vhost %>;
listen 443 ssl http2;
<% if (hasIPv6) { -%>
listen [::]:443 ssl http2;
<% } -%>
<% } else { -%>
listen 443 ssl http2 default_server;
<% if (hasIPv6) { -%>
listen [::]:443 ssl http2 default_server;
<% } -%>
<% } -%>
server_tokens off; # hide version
@@ -100,7 +95,7 @@ server {
# enable for proxied requests as well
gzip_proxied any;
<% if ( endpoint === 'admin' || endpoint === 'ip' || endpoint === 'setup' ) { -%>
<% if ( endpoint === 'admin' ) { -%>
# CSP headers for the admin/dashboard resources
add_header Content-Security-Policy "default-src 'none'; frame-src 'self' cloudron.io *.cloudron.io; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
<% } else { %>
@@ -177,7 +172,7 @@ server {
}
<% } %>
<% if ( endpoint === 'admin' || endpoint === 'setup' ) { %>
<% if ( endpoint === 'admin' ) { %>
location /api/ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 1m;
@@ -222,11 +217,6 @@ server {
# redirect everything to the app. this is temporary because there is no way
# to clear a permanent redirect on the browser
return 302 https://<%= redirectTo %>$request_uri;
<% } else if ( endpoint === 'ip' ) { %>
location / {
root <%= sourceDir %>/dashboard/dist;
try_files /splash.html =404;
}
<% } %>
}
}
+11 -12
View File
@@ -155,12 +155,12 @@ function oomEvent(eventId, app, addon, containerId, event, callback) {
let title, message, program;
if (app) {
program = `App ${app.fqdn}`;
title = `The application at ${app.fqdn} ran out of memory.`;
message = 'The application has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://docs.cloudron.io/apps/#memory-limit)';
title = `The application ${app.fqdn} (${app.manifest.title}) ran out of memory.`;
message = 'The application has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://cloudron.io/documentation/apps/#memory-limit)';
} else if (addon) {
program = `${addon.name} service`;
title = `The ${addon.name} service ran out of memory`;
message = 'The service has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://docs.cloudron.io/troubleshooting/#services)';
message = 'The service has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://cloudron.io/documentation/troubleshooting/#services)';
} else { // this never happens currently
program = `Container ${containerId}`;
title = `The container ${containerId} ran out of memory`;
@@ -181,7 +181,7 @@ function appUp(eventId, app, callback) {
actionForAllAdmins([], function (admin, done) {
mailer.appUp(admin.email, app);
add(admin.id, eventId, `App ${app.fqdn} is back online`, `The application installed at ${app.fqdn} is back online.`, done);
add(admin.id, eventId, `App ${app.fqdn} is back online`, `The application ${app.manifest.title} installed at ${app.fqdn} is back online.`, done);
}, callback);
}
@@ -192,7 +192,7 @@ function appDied(eventId, app, callback) {
actionForAllAdmins([], function (admin, callback) {
mailer.appDied(admin.email, app);
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application installed at ${app.fqdn} is not responding.`, callback);
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application ${app.manifest.title} installed at ${app.fqdn} is not responding.`, callback);
}, callback);
}
@@ -201,19 +201,17 @@ function appUpdated(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!app.appStoreId) return callback(); // skip notification of dev apps
const tmp = app.manifest.description.match(/<upstream>(.*)<\/upstream>/i);
const upstreamVersion = (tmp && tmp[1]) ? tmp[1] : '';
const title = upstreamVersion ? `${app.manifest.title} at ${app.fqdn} updated to ${upstreamVersion} (package version ${app.manifest.version})`
: `${app.manifest.title} at ${app.fqdn} updated to package version ${app.manifest.version}`;
actionForAllAdmins([], function (admin, done) {
add(admin.id, eventId, title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`, function (error) {
add(admin.id, eventId, title, `The application ${app.manifest.title} installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`, function (error) {
if (error) return callback(error);
mailer.appUpdated(admin.email, app, function (error) {
if (error) debug('appUpdated: Failed to send app updated email', error); // non fatal
if (error) console.error('Failed to send app updated email', error); // non fatal
done();
});
});
@@ -241,7 +239,7 @@ function boxUpdateError(eventId, errorMessage, callback) {
actionForAllAdmins([], function (admin, done) {
mailer.boxUpdateError(admin.email, errorMessage);
add(admin.id, eventId, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}.`, done);
add(admin.id, eventId, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}. Update will be retried in 4 hours`, done);
}, callback);
}
@@ -265,7 +263,7 @@ function backupFailed(eventId, taskId, errorMessage, callback) {
actionForAllAdmins([], function (admin, callback) {
mailer.backupFailed(admin.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
add(admin.id, eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}).`, callback);
add(admin.id, eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}). Will be retried in 4 hours`, callback);
}, callback);
}
@@ -303,7 +301,7 @@ function alert(id, title, message, callback) {
});
});
}, function (error) {
if (error) debug('alert: error notifying', error);
if (error) console.error(error);
callback();
});
@@ -340,6 +338,7 @@ function onEvent(id, action, source, data, callback) {
return appUp(id, data.app, callback);
case eventlog.ACTION_APP_UPDATE_FINISH:
if (!data.app.appStoreId) return callback(); // skip notification of dev apps
return appUpdated(id, data.app, callback);
case eventlog.ACTION_CERTIFICATE_RENEWAL:
-3
View File
@@ -46,13 +46,10 @@ exports = module.exports = {
CLOUDRON_AVATAR_FILE: path.join(baseDir(), 'boxdata/avatar.png'),
UPDATE_CHECKER_FILE: path.join(baseDir(), 'boxdata/updatechecker.json'),
ADDON_TURN_SECRET_FILE: path.join(baseDir(), 'boxdata/addon-turn-secret'),
FIREWALL_BLOCKLIST_FILE: path.join(baseDir(), 'boxdata/firewall/blocklist.txt'),
FIREWALL_CONFIG_FILE: path.join(baseDir(), 'boxdata/firewall-config.json'),
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'),
BOX_LOG_FILE: path.join(baseDir(), 'platformdata/logs/box.log'),
GHOST_USER_FILE: path.join(baseDir(), 'platformdata/cloudron_ghost.json'),
+54 -30
View File
@@ -2,7 +2,7 @@
exports = module.exports = {
start: start,
stopAllTasks: stopAllTasks,
stop: stop,
// exported for testing
_isReady: false
@@ -26,6 +26,8 @@ var addons = require('./addons.js'),
tasks = require('./tasks.js'),
_ = require('underscore');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function start(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -43,7 +45,7 @@ function start(callback) {
if (_.isEqual(infra, existingInfra)) {
debug('platform is uptodate at version %s', infra.version);
onPlatformReady(false /* !infraChanged */);
onPlatformReady();
return callback();
}
@@ -54,8 +56,9 @@ function start(callback) {
if (error) return callback(error);
async.series([
(next) => { if (existingInfra.version !== infra.version) removeAllContainers(existingInfra, next); else next(); },
markApps.bind(null, existingInfra), // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
stopContainers.bind(null, existingInfra),
// mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
startApps.bind(null, existingInfra),
graphs.startGraphite.bind(null, existingInfra),
sftp.startSftp.bind(null, existingInfra),
addons.startServices.bind(null, existingInfra),
@@ -65,36 +68,41 @@ function start(callback) {
locker.unlock(locker.OP_PLATFORM_START);
onPlatformReady(true /* infraChanged */);
onPlatformReady();
callback();
});
}
function stopAllTasks(callback) {
function stop(callback) {
tasks.stopAllTasks(callback);
}
function onPlatformReady(infraChanged) {
debug(`onPlatformReady: platform is ready. infra changed: ${infraChanged}`);
function onPlatformReady() {
debug('onPlatformReady: platform is ready');
exports._isReady = true;
let tasks = [ apps.schedulePendingTasks ];
if (infraChanged) tasks.push(applyPlatformConfig, pruneInfraImages);
apps.schedulePendingTasks(NOOP_CALLBACK);
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}`);
});
});
applyPlatformConfig(NOOP_CALLBACK);
pruneInfraImages(NOOP_CALLBACK);
}
function applyPlatformConfig(callback) {
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(error);
// 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
addons.updateServiceConfig(platformConfig, callback);
});
async.retry({ times: 10, interval: 5 * 60 * 1000 }, function (retryCallback) {
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return retryCallback(error);
addons.updateServiceConfig(platformConfig, function (error) {
if (error) debug('Error updating services. Will rety in 5 minutes', platformConfig, error);
retryCallback(error);
});
});
}, callback);
}
function pruneInfraImages(callback) {
@@ -122,21 +130,37 @@ function pruneInfraImages(callback) {
}, callback);
}
function removeAllContainers(existingInfra, callback) {
debug('removeAllContainers: removing all containers for infra upgrade');
function stopContainers(existingInfra, callback) {
// always stop addons to restart them on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
debug('stopping all containers for infra upgrade');
async.series([
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop'),
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f')
], callback);
} else {
assert(typeof infra.images, 'object');
var changedAddons = [ ];
for (var imageName in existingInfra.images) { // do not use infra.images because we can only stop things which are existing
if (infra.images[imageName].tag !== existingInfra.images[imageName].tag) changedAddons.push(imageName);
}
async.series([
shell.exec.bind(null, 'removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop'),
shell.exec.bind(null, 'removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f')
], callback);
debug('stopContainer: stopping addons for incremental infra update: %j', changedAddons);
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
// ignore error if container not found (and fail later) so that this code works across restarts
async.series([
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'label=isCloudronManaged' | xargs --no-run-if-empty docker stop || true`),
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'label=isCloudronManaged' | xargs --no-run-if-empty docker rm -f || true`)
], callback);
}
}
function markApps(existingInfra, callback) {
function startApps(existingInfra, callback) {
if (existingInfra.version === 'none') { // cloudron is being restored from backup
debug('markApps: restoring installed apps');
debug('startApps: restoring installed apps');
apps.restoreInstalledApps(callback);
} else if (existingInfra.version !== infra.version) {
debug('markApps: reconfiguring installed apps');
debug('startApps: reconfiguring installed apps');
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
apps.configureInstalledApps(callback);
} else {
@@ -148,10 +172,10 @@ function markApps(existingInfra, callback) {
if (changedAddons.length) {
// restart apps if docker image changes since the IP changes and any "persistent" connections fail
debug(`markApps: changedAddons: ${JSON.stringify(changedAddons)}`);
debug(`startApps: changedAddons: ${JSON.stringify(changedAddons)}`);
apps.restartAppsUsingAddons(changedAddons, callback);
} else {
debug('markApps: apps are already uptodate');
debug('startApps: apps are already uptodate');
callback();
}
}
+18 -24
View File
@@ -54,7 +54,7 @@ function unprovision(callback) {
// TODO: also cancel any existing configureWebadmin task
async.series([
settings.setAdminLocation.bind(null, '', ''),
settings.setAdmin.bind(null, '', ''),
mail.clearDomains,
domains.clear
], callback);
@@ -98,24 +98,24 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
dkimSelector: 'cloudron'
};
async.series([
settings.setMailLocation.bind(null, domain, `${constants.ADMIN_LOCATION}.${domain}`), // default mail location. do this before we add the domain for upserting mail DNS
domains.add.bind(null, domain, data, auditSource),
sysinfo.testConfig.bind(null, sysinfoConfig)
], function (error) {
domains.add(domain, data, auditSource, function (error) {
if (error) return done(error);
callback(); // now that args are validated run the task in the background
sysinfo.testConfig(sysinfoConfig, function (error) {
if (error) return done(error);
async.series([
settings.setSysinfoConfig.bind(null, sysinfoConfig),
cloudron.setupDnsAndCert.bind(null, constants.ADMIN_LOCATION, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
callback(); // now that args are validated run the task in the background
async.series([
settings.setSysinfoConfig.bind(null, sysinfoConfig),
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
});
});
});
});
@@ -162,7 +162,7 @@ function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, ca
assert.strictEqual(typeof callback, 'function');
if (!semver.valid(version)) return callback(new BoxError(BoxError.BAD_FIELD, 'version is not a valid semver', { field: 'version' }));
if (constants.VERSION !== version) return callback(new BoxError(BoxError.BAD_STATE, `Run "cloudron-setup --version ${version}" on a fresh Ubuntu installation to restore from this backup`));
if (constants.VERSION !== version) return callback(new BoxError(BoxError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring'));
@@ -199,13 +199,7 @@ function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, ca
setProgress.bind(null, 'restore', 'Downloading backup'),
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
settings.setSysinfoConfig.bind(null, sysinfoConfig),
(done) => {
const adminDomain = settings.adminDomain(); // load this fresh from after the backup.restore
async.series([
cloudron.setupDnsAndCert.bind(null, constants.ADMIN_LOCATION, adminDomain, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, adminDomain, auditSource)
], done);
},
cloudron.setupDashboard.bind(null, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
settings.setBackupCredentials.bind(null, backupConfig), // update just the credentials and not the policy and flags
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
], function (error) {
BIN
View File
Binary file not shown.
+109 -125
View File
@@ -1,33 +1,33 @@
'use strict';
exports = module.exports = {
setFallbackCertificate,
getFallbackCertificate,
setFallbackCertificate: setFallbackCertificate,
getFallbackCertificate: getFallbackCertificate,
generateFallbackCertificateSync,
setAppCertificateSync,
generateFallbackCertificateSync: generateFallbackCertificateSync,
setAppCertificateSync: setAppCertificateSync,
validateCertificate,
validateCertificate: validateCertificate,
getCertificate,
ensureCertificate,
getCertificate: getCertificate,
ensureCertificate: ensureCertificate,
renewCerts,
renewCerts: renewCerts,
// the 'configure' ensure a certificate and generate nginx config
configureAdmin,
configureApp,
unconfigureApp,
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
// these only generate nginx config
writeDefaultConfig,
writeDashboardConfig,
writeAppConfig,
writeDefaultConfig: writeDefaultConfig,
writeAdminConfig: writeAdminConfig,
writeAppConfig: writeAppConfig,
removeAppConfigs,
removeAppConfigs: removeAppConfigs,
// exported for testing
_getAcmeApi: getAcmeApi
_getCertApi: getCertApi
};
var acme2 = require('./cert/acme2.js'),
@@ -35,12 +35,14 @@ var acme2 = require('./cert/acme2.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
caas = require('./cert/caas.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:reverseproxy'),
domains = require('./domains.js'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'),
mail = require('./mail.js'),
os = require('os'),
@@ -57,16 +59,20 @@ var acme2 = require('./cert/acme2.js'),
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
function getAcmeApi(domainObject, callback) {
function getCertApi(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const api = acme2;
if (domainObject.tlsConfig.provider === 'fallback') return callback(null, fallback, { fallback: true });
let options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
options.wildcard = !!domainObject.tlsConfig.wildcard;
var api = domainObject.tlsConfig.provider === 'caas' ? caas : acme2;
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
if (domainObject.tlsConfig.provider !== 'caas') {
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
options.wildcard = !!domainObject.tlsConfig.wildcard;
}
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
@@ -102,6 +108,8 @@ function providerMatchesSync(domainObject, certFilePath, apiOptions) {
if (!fs.existsSync(certFilePath)) return false; // not found
if (apiOptions.fallback) return certFilePath.includes('.host.cert');
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
if (!subjectAndIssuer) return false; // something bad happenned
@@ -179,9 +187,9 @@ function generateFallbackCertificateSync(domainObject) {
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
let opensslConfWithSan;
let cn = domain;
let cn = domainObject.config.hyphenatedSubdomains ? domains.parentDomain(domain) : domain;
debug(`generateFallbackCertificateSync: domain=${domainObject.domain} cn=${cn}`);
debug(`generateFallbackCertificateSync: domain=${domainObject.domain} cn=${cn} hyphenated=${domainObject.config.hyphenatedSubdomains}`);
opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${cn}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
@@ -207,9 +215,15 @@ function setFallbackCertificate(domain, fallback, callback) {
assert.strictEqual(typeof fallback, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`setFallbackCertificate: setting certs for domain ${domain}`);
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
if (fallback.restricted) { // restricted certs are not backed up
debug(`setFallbackCertificate: setting restricted certs for domain ${domain}`);
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
} else {
debug(`setFallbackCertificate: setting certs for domain ${domain}`);
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
}
// TODO: maybe the cert is being used by the mail container
reload(function (error) {
@@ -223,8 +237,15 @@ function getFallbackCertificate(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
const certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
const keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
// check for any pre-provisioned (caas) certs. they get first priority
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
var keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
// check for auto-generated or user set fallback certs
certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
callback(null, { certFilePath, keyFilePath });
}
@@ -246,12 +267,15 @@ function setAppCertificateSync(location, domainObject, certificate) {
return null;
}
function getAcmeCertificate(hostname, domainObject, callback) {
function getCertificateByHostname(hostname, domainObject, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
let certFilePath, keyFilePath;
let certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.cert`);
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
if (hostname !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
let certName = domains.makeWildcard(hostname).replace('*.', '_.');
@@ -274,22 +298,10 @@ function getCertificate(fqdn, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// 1. user cert always wins
// 2. if using fallback provider, return that cert
// 3. look for LE certs
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
// user cert always wins
let certFilePath = path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`);
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
if (domainObject.tlsConfig.provider === 'fallback') return getFallbackCertificate(domain, callback);
getAcmeCertificate(fqdn, domainObject, function (error, result) {
getCertificateByHostname(fqdn, domainObject, function (error, result) {
if (error || result) return callback(error, result);
return getFallbackCertificate(domain, callback);
@@ -297,6 +309,17 @@ function getCertificate(fqdn, domain, callback) {
});
}
function notifyCertChanged(vhost, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`notifyCertChanged: vhost: ${vhost} mailFqdn: ${settings.mailFqdn()}`);
if (vhost !== settings.mailFqdn()) return callback();
mail.handleCertChanged(callback);
}
function ensureCertificate(vhost, domain, auditSource, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof domain, 'string');
@@ -306,32 +329,14 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
// user cert always wins
let certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug(`ensureCertificate: ${vhost} will use custom app certs`);
return callback(null, { certFilePath, keyFilePath }, { renewed: false });
}
if (domainObject.tlsConfig.provider === 'fallback') {
debug(`ensureCertificate: ${vhost} will use fallback certs`);
return getFallbackCertificate(domain, function (error, bundle) {
if (error) return callback(error);
callback(null, bundle, { renewed: false });
});
}
getAcmeApi(domainObject, function (error, acmeApi, apiOptions) {
getCertApi(domainObject, function (error, api, apiOptions) {
if (error) return callback(error);
getAcmeCertificate(vhost, domainObject, function (_error, currentBundle) {
getCertificateByHostname(vhost, domainObject, function (_error, currentBundle) {
if (currentBundle) {
debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`);
if (currentBundle.certFilePath.endsWith('.user.cert')) return callback(null, currentBundle, { renewed: false }); // user certs cannot be renewed
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle, { renewed: false });
debug(`ensureCertificate: ${vhost} cert require renewal`);
} else {
@@ -340,7 +345,7 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
acmeApi.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) {
api.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) {
debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${certFilePath || 'null'}`);
eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '' });
@@ -350,14 +355,19 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
return callback(null, currentBundle, { renewed: false });
}
if (certFilePath && keyFilePath) return callback(null, { certFilePath, keyFilePath }, { renewed: true });
debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`);
getFallbackCertificate(domain, function (error, bundle) {
notifyCertChanged(vhost, function (error) {
if (error) return callback(error);
callback(null, bundle, { renewed: false });
if (certFilePath && keyFilePath) return callback(null, { certFilePath, keyFilePath }, { renewed: true });
debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`);
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
getFallbackCertificate(domain, function (error, bundle) {
if (error) return callback(error);
callback(null, bundle, { renewed: false });
});
});
});
});
@@ -365,7 +375,7 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
});
}
function writeDashboardNginxConfig(bundle, configFileName, vhost, callback) {
function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
@@ -374,7 +384,7 @@ function writeDashboardNginxConfig(bundle, configFileName, vhost, callback) {
var data = {
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: settings.adminOrigin(),
vhost: vhost,
vhost: vhost, // if vhost is empty it will become the default_server
hasIPv6: sysinfo.hasIPv6(),
endpoint: 'admin',
certFilePath: bundle.certFilePath,
@@ -402,16 +412,16 @@ function configureAdmin(domain, auditSource, callback) {
ensureCertificate(adminFqdn, domainObject.domain, auditSource, function (error, bundle) {
if (error) return callback(error);
writeDashboardNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
writeAdminNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
});
});
}
function writeDashboardConfig(domain, callback) {
function writeAdminConfig(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`writeDashboardConfig: writing admin config for ${domain}`);
debug(`writeAdminConfig: writing admin config for ${domain}`);
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
@@ -421,7 +431,7 @@ function writeDashboardConfig(domain, callback) {
getCertificate(adminFqdn, domainObject.domain, function (error, bundle) {
if (error) return callback(error);
writeDashboardNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
writeAdminNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
});
});
}
@@ -567,13 +577,8 @@ function renewCerts(options, auditSource, progressCallback, callback) {
var appDomains = [];
// add webadmin and mail domain
if (settings.mailFqdn() === settings.adminFqdn()) {
appDomains.push({ domain: settings.adminDomain(), fqdn: settings.adminFqdn(), type: 'webadmin+mail', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.adminFqdn()}.conf`) });
} else {
appDomains.push({ domain: settings.adminDomain(), fqdn: settings.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.adminFqdn()}.conf`) });
appDomains.push({ domain: settings.mailDomain(), fqdn: settings.mailFqdn(), type: 'mail' });
}
// add webadmin domain
appDomains.push({ domain: settings.adminDomain(), fqdn: settings.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.adminFqdn()}.conf`) });
// add app main
allApps.forEach(function (app) {
@@ -582,8 +587,8 @@ function renewCerts(options, auditSource, progressCallback, callback) {
appDomains.push({ domain: app.domain, fqdn: app.fqdn, type: 'main', app: app, nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf') });
app.alternateDomains.forEach(function (alternateDomain) {
const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}-redirect-${alternateDomain.fqdn}.conf`);
appDomains.push({ domain: alternateDomain.domain, fqdn: alternateDomain.fqdn, type: 'alternate', app: app, nginxConfigFilename });
let nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}-redirect-${alternateDomain.fqdn}.conf`);
appDomains.push({ domain: alternateDomain.domain, fqdn: alternateDomain.fqdn, type: 'alternate', app: app, nginxConfigFilename: nginxConfigFilename });
});
});
@@ -600,8 +605,6 @@ function renewCerts(options, auditSource, progressCallback, callback) {
if (state.renewed) renewed.push(appDomain.fqdn);
if (appDomain.type === 'mail') return iteratorCallback(); // mail has no nginx config to check current cert
// hack to check if the app's cert changed or not. this doesn't handle prod/staging le change since they use same file name
let currentNginxConfig = safe.fs.readFileSync(appDomain.nginxConfigFilename, 'utf8') || '';
if (currentNginxConfig.includes(bundle.certFilePath)) return iteratorCallback();
@@ -609,20 +612,13 @@ function renewCerts(options, auditSource, progressCallback, callback) {
debug(`renewCerts: creating new nginx config since ${appDomain.nginxConfigFilename} does not have ${bundle.certFilePath}`);
// reconfigure since the cert changed
if (appDomain.type === 'webadmin') {
return writeDashboardNginxConfig(bundle, `${settings.adminFqdn()}.conf`, settings.adminFqdn(), iteratorCallback);
} else if (appDomain.type === 'webadmin+mail') {
return async.series([
mail.handleCertChanged,
writeDashboardNginxConfig.bind(null, bundle, `${settings.adminFqdn()}.conf`, settings.adminFqdn())
], iteratorCallback);
} else if (appDomain.type === 'main') {
return writeAppNginxConfig(appDomain.app, bundle, iteratorCallback);
} else if (appDomain.type === 'alternate') {
return writeAppRedirectNginxConfig(appDomain.app, appDomain.fqdn, bundle, iteratorCallback);
}
var configureFunc;
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${settings.adminFqdn()}.conf`, settings.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppNginxConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectNginxConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
else return iteratorCallback(new BoxError(BoxError.INTERNAL_ERROR, `Unknown domain type for ${appDomain.fqdn}. This should never happen`));
iteratorCallback(new BoxError(BoxError.INTERNAL_ERROR, `Unknown domain type for ${appDomain.fqdn}. This should never happen`));
configureFunc(iteratorCallback);
});
}, function (error) {
if (error) return callback(error);
@@ -630,10 +626,8 @@ function renewCerts(options, auditSource, progressCallback, callback) {
debug(`renewCerts: Renewed certs of ${JSON.stringify(renewed)}`);
if (renewed.length === 0) return callback(null);
async.series([
(next) => { return renewed.includes(settings.mailFqdn()) ? mail.handleCertChanged(next) : next(); },// mail cert renewed
reload // reload nginx if any certs were updated but the config was not rewritten
], callback);
// reload nginx if any certs were updated but the config was not rewritten
reload(callback);
});
});
}
@@ -646,37 +640,27 @@ function removeAppConfigs() {
}
}
function writeDefaultConfig(options, callback) {
assert.strictEqual(typeof options, 'object');
function writeDefaultConfig(callback) {
assert.strictEqual(typeof callback, 'function');
const certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
const keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
debug('writeDefaultConfig: create new cert');
const cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=${cn} -nodes`)) {
debug(`writeDefaultConfig: could not generate certificate: ${safe.error.message}`);
return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
}
}
const data = {
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: settings.adminOrigin(),
vhost: '',
hasIPv6: sysinfo.hasIPv6(),
endpoint: options.activated ? 'ip' : 'setup',
certFilePath,
keyFilePath,
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_DEFAULT_CONFIG_FILE_NAME);
writeAdminNginxConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
if (error) return callback(error);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
debug('writeDefaultConfig: done');
reload(callback);
callback(null);
});
}
+1 -10
View File
@@ -3,8 +3,7 @@
exports = module.exports = {
list: list,
startBackup: startBackup,
cleanup: cleanup,
check: check
cleanup: cleanup
};
let auditSource = require('../auditsource.js'),
@@ -42,11 +41,3 @@ function cleanup(req, res, next) {
next(new HttpSuccess(202, { taskId }));
});
}
function check(req, res, next) {
backups.checkConfiguration(function (error, message) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { ok: !message, message: message }));
});
}
+28 -24
View File
@@ -1,29 +1,30 @@
'use strict';
exports = module.exports = {
login,
logout,
passwordResetRequest,
passwordReset,
setupAccount,
reboot,
isRebootRequired,
getConfig,
getDisks,
getMemory,
getUpdateInfo,
update,
checkForUpdates,
getLogs,
getLogStream,
updateDashboardDomain,
prepareDashboardDomain,
renewCerts,
getServerIp,
syncExternalLdap
login: login,
logout: logout,
passwordResetRequest: passwordResetRequest,
passwordReset: passwordReset,
setupAccount: setupAccount,
reboot: reboot,
isRebootRequired: isRebootRequired,
getConfig: getConfig,
getDisks: getDisks,
getMemory: getMemory,
getUpdateInfo: getUpdateInfo,
update: update,
checkForUpdates: checkForUpdates,
getLogs: getLogs,
getLogStream: getLogStream,
setDashboardAndMailDomain: setDashboardAndMailDomain,
prepareDashboardDomain: prepareDashboardDomain,
renewCerts: renewCerts,
getServerIp: getServerIp,
syncExternalLdap: syncExternalLdap
};
let assert = require('assert'),
async = require('async'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
cloudron = require('../cloudron.js'),
@@ -102,7 +103,7 @@ function passwordReset(req, res, next) {
if (error) return next(new HttpError(401, 'Invalid resetToken'));
// if you fix the duration here, the emails and UI have to be fixed as well
if (Date.now() - userObject.resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
if (!userObject.username) return next(new HttpError(409, 'No username set'));
// setPassword clears the resetToken
@@ -203,7 +204,10 @@ function checkForUpdates(req, res, next) {
// it can take a while sometimes to get all the app updates one by one
req.clearTimeout();
updateChecker.checkForUpdates({ automatic: false }, function () {
async.series([
(done) => updateChecker.checkAppUpdates({ automatic: false }, done),
(done) => updateChecker.checkBoxUpdates({ automatic: false }, done),
], function () {
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
});
}
@@ -270,10 +274,10 @@ function getLogStream(req, res, next) {
});
}
function updateDashboardDomain(req, res, next) {
function setDashboardAndMailDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
cloudron.updateDashboardDomain(req.body.domain, auditSource.fromRequest(req), function (error) {
cloudron.setDashboardAndMailDomain(req.body.domain, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204, {}));
+4
View File
@@ -24,6 +24,7 @@ function add(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be a string'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('hyphenatedSubdomains' in req.body.config && typeof req.body.config.hyphenatedSubdomains !== 'boolean') return next(new HttpError(400, 'hyphenatedSubdomains must be a boolean'));
if ('wildcard' in req.body.config && typeof req.body.config.wildcard !== 'boolean') return next(new HttpError(400, 'wildcard must be a boolean'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
@@ -32,6 +33,7 @@ function add(req, res, next) {
let fallbackCertificate = req.body.fallbackCertificate;
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('restricted' in fallbackCertificate && typeof fallbackCertificate.restricted !== 'boolean') return next(new HttpError(400, 'fallbackCertificate.restricted must be a boolean'));
}
if ('tlsConfig' in req.body) {
@@ -84,6 +86,7 @@ function update(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be an object'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('hyphenatedSubdomains' in req.body.config && typeof req.body.config.hyphenatedSubdomains !== 'boolean') return next(new HttpError(400, 'hyphenatedSubdomains must be a boolean'));
if ('wildcard' in req.body.config && typeof req.body.config.wildcard !== 'boolean') return next(new HttpError(400, 'wildcard must be a boolean'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
@@ -92,6 +95,7 @@ function update(req, res, next) {
let fallbackCertificate = req.body.fallbackCertificate;
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('restricted' in fallbackCertificate && typeof fallbackCertificate.restricted !== 'boolean') return next(new HttpError(400, 'fallbackCertificate.restricted must be a boolean'));
}
if ('tlsConfig' in req.body) {
+1 -1
View File
@@ -71,7 +71,7 @@ function updateMembers(req, res, next) {
}
function list(req, res, next) {
groups.getAllWithMembers(function (error, result) {
groups.getAll(function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { groups: result }));
-1
View File
@@ -15,7 +15,6 @@ exports = module.exports = {
groups: require('./groups.js'),
mail: require('./mail.js'),
mailserver: require('./mailserver.js'),
network: require('./network.js'),
notifications: require('./notifications.js'),
profile: require('./profile.js'),
provision: require('./provision.js'),
+1 -18
View File
@@ -11,7 +11,6 @@ exports = module.exports = {
setCatchAllAddress,
setMailRelay,
setMailEnabled,
setBanner,
sendTestMail,
@@ -222,9 +221,7 @@ function removeMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
if (typeof req.body.deleteMails !== 'boolean') return next(new HttpError(400, 'deleteMails must be a boolean'));
mail.removeMailbox(req.params.name, req.params.domain, req.body, auditSource.fromRequest(req), function (error) {
mail.removeMailbox(req.params.name, req.params.domain, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, {}));
@@ -262,20 +259,6 @@ function setAliases(req, res, next) {
});
}
function setBanner(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.text !== 'string') return res.status(400).send({ message: 'text must be a string' });
if ('html' in req.body && typeof req.body.html !== 'string') return res.status(400).send({ message: 'html must be a string' });
mail.setBanner(req.params.domain, { text: req.body.text, html: req.body.html || null }, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202));
});
}
function getLists(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
+5 -31
View File
@@ -1,25 +1,20 @@
'use strict';
exports = module.exports = {
proxy,
getLocation,
setLocation
proxy
};
var addons = require('../addons.js'),
assert = require('assert'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
mail = require('../mail.js'),
middleware = require('../middleware/index.js'),
HttpError = require('connect-lastmile').HttpError,
url = require('url');
function proxy(req, res, next) {
assert.strictEqual(typeof req.params.pathname, 'string');
let parsedUrl = url.parse(req.url, true /* parseQueryString */);
const pathname = req.path.split('/').pop();
// do not proxy protected values
delete parsedUrl.query['access_token'];
@@ -30,7 +25,7 @@ function proxy(req, res, next) {
if (error) return next(BoxError.toHttpError(error));
parsedUrl.query['access_token'] = addonDetails.token;
req.url = url.format({ pathname: pathname, query: parsedUrl.query });
req.url = url.format({ pathname: req.params.pathname, query: parsedUrl.query });
const proxyOptions = url.parse(`https://${addonDetails.ip}:3000`);
proxyOptions.rejectUnauthorized = false;
@@ -46,24 +41,3 @@ function proxy(req, res, next) {
});
});
}
function getLocation(req, res, next) {
mail.getLocation(function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { domain: result.domain, subdomain: result.subdomain }));
});
}
function setLocation(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
if (typeof req.body.subdomain !== 'string') return next(new HttpError(400, 'subdomain must be a string'));
mail.setLocation(req.body.subdomain, req.body.domain, auditSource.fromRequest(req), function (error, taskId) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId }));
});
}
-35
View File
@@ -1,35 +0,0 @@
'use strict';
exports = module.exports = {
getBlocklist,
setBlocklist
};
var assert = require('assert'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
network = require('../network.js');
function getBlocklist(req, res, next) {
network.getBlocklist(function (error, blocklist) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { blocklist }));
});
}
function setBlocklist(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.blocklist !== 'string') return next(new HttpError(400, 'blocklist must be a string'));
req.clearTimeout(); // can take a while if there is a lot of network ranges
network.setBlocklist(req.body.blocklist, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
+29 -25
View File
@@ -17,20 +17,40 @@ var assert = require('assert'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
settings = require('../settings.js');
function getAutoupdatePattern(req, res, next) {
settings.getAutoupdatePattern(function (error, pattern) {
function getAppAutoupdatePattern(req, res, next) {
settings.getAppAutoupdatePattern(function (error, pattern) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { pattern: pattern }));
});
}
function setAutoupdatePattern(req, res, next) {
function setAppAutoupdatePattern(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setAutoupdatePattern(req.body.pattern, function (error) {
settings.setAppAutoupdatePattern(req.body.pattern, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function getBoxAutoupdatePattern(req, res, next) {
settings.getBoxAutoupdatePattern(function (error, pattern) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { pattern: pattern }));
});
}
function setBoxAutoupdatePattern(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setBoxAutoupdatePattern(req.body.pattern, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -77,30 +97,12 @@ function setBackupConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.schedulePattern !== 'string') return next(new HttpError(400, 'schedulePattern is required'));
if (typeof req.body.intervalSecs !== 'number') return next(new HttpError(400, 'intervalSecs is required'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
if ('syncConcurrency' in req.body) {
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
if (req.body.syncConcurrency < 1) return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
}
if ('copyConcurrency' in req.body) {
if (typeof req.body.copyConcurrency !== 'number') return next(new HttpError(400, 'copyConcurrency must be a positive integer'));
if (req.body.copyConcurrency < 1) return next(new HttpError(400, 'copyConcurrency must be a positive integer'));
}
if ('downloadConcurrency' in req.body) {
if (typeof req.body.downloadConcurrency !== 'number') return next(new HttpError(400, 'downloadConcurrency must be a positive integer'));
if (req.body.downloadConcurrency < 1) return next(new HttpError(400, 'downloadConcurrency must be a positive integer'));
}
if ('deleteConcurrency' in req.body) {
if (typeof req.body.deleteConcurrency !== 'number') return next(new HttpError(400, 'deleteConcurrency must be a positive integer'));
if (req.body.deleteConcurrency < 1) return next(new HttpError(400, 'deleteConcurrency must be a positive integer'));
}
if ('uploadPartSize' in req.body) {
if (typeof req.body.uploadPartSize !== 'number') return next(new HttpError(400, 'uploadPartSize must be a positive integer'));
if (req.body.uploadPartSize < 1) return next(new HttpError(400, 'uploadPartSize must be a positive integer'));
}
if ('memoryLimit' in req.body && typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit must be a positive integer'));
if (typeof req.body.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in req.body && typeof req.body.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
@@ -285,7 +287,8 @@ function get(req, res, next) {
case settings.REGISTRY_CONFIG_KEY: return getRegistryConfig(req, res, next);
case settings.SYSINFO_CONFIG_KEY: return getSysinfoConfig(req, res, next);
case settings.AUTOUPDATE_PATTERN_KEY: return getAutoupdatePattern(req, res, next);
case settings.APP_AUTOUPDATE_PATTERN_KEY: return getAppAutoupdatePattern(req, res, next);
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return getBoxAutoupdatePattern(req, res, next);
case settings.TIME_ZONE_KEY: return getTimeZone(req, res, next);
case settings.DIRECTORY_CONFIG_KEY: return getDirectoryConfig(req, res, next);
@@ -306,7 +309,8 @@ function set(req, res, next) {
case settings.REGISTRY_CONFIG_KEY: return setRegistryConfig(req, res, next);
case settings.SYSINFO_CONFIG_KEY: return setSysinfoConfig(req, res, next);
case settings.AUTOUPDATE_PATTERN_KEY: return setAutoupdatePattern(req, res, next);
case settings.APP_AUTOUPDATE_PATTERN_KEY: return setAppAutoupdatePattern(req, res, next);
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return setBoxAutoupdatePattern(req, res, next);
case settings.TIME_ZONE_KEY: return setTimeZone(req, res, next);
case settings.DIRECTORY_CONFIG_KEY: return setDirectoryConfig(req, res, next);
-1
View File
@@ -41,7 +41,6 @@ function createTicket(req, res, next) {
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
if (req.body.altEmail && typeof req.body.altEmail !== 'string') return next(new HttpError(400, 'altEmail must be string'));
if (req.body.enableSshSupport && typeof req.body.enableSshSupport !== 'boolean') return next(new HttpError(400, 'enableSshSupport must be a boolean'));
settings.getSupportConfig(function (error, supportConfig) {
if (error) return next(new HttpError(503, `Error getting support config: ${error.message}`));
+1 -1
View File
@@ -28,7 +28,7 @@ function setup(done) {
database._clear,
settings._setApiServerOrigin.bind(null, 'http://localhost:6060'),
settings.setAdminLocation.bind(null, 'appstore-test.example.com', 'my.appstore-test.example.com'),
settings.setAdmin.bind(null, 'appstore-test.example.com', 'my.appstore-test.example.com'),
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
+1 -32
View File
@@ -62,7 +62,7 @@ function setup(done) {
},
function createSettings(callback) {
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz', retentionPolicy: { keepWithinSecs: 2 * 24 * 60 * 60 }, schedulePattern: '00 00 23 * * *' }, callback);
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz', retentionPolicy: { keepWithinSecs: 2 * 24 * 60 * 60 } }, callback);
}
], done);
}
@@ -108,35 +108,4 @@ describe('Backups API', function () {
});
});
});
describe('check', function () {
it('fails due to mising token', function (done) {
superagent.get(SERVER_URL + '/api/v1/backups/check')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/backups/check')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/backups/check')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.ok).to.equal(false);
expect(result.body.message).to.not.be.empty();
done();
});
});
});
});
+1 -1
View File
@@ -32,7 +32,7 @@ function setup(done) {
server.start.bind(server),
database._clear,
settings._setApiServerOrigin.bind(null, 'http://localhost:6060'),
settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz', retentionPolicy: { keepWithinSecs: 10000 }, schedulePattern: '00 00 23 * * *' })
settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz', retentionPolicy: { keepWithinSecs: 10000 } })
], done);
}
+2 -4
View File
@@ -625,7 +625,6 @@ describe('Mail API', function () {
it('disable fails even if not exist', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + 'someuserdoesnotexist')
.send({ deleteMails: false })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
@@ -635,7 +634,6 @@ describe('Mail API', function () {
it('disable succeeds', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + MAILBOX_NAME)
.send({ deleteMails: false })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
@@ -651,7 +649,7 @@ describe('Mail API', function () {
describe('aliases', function () {
after(function (done) {
mail._removeMailboxes(DOMAIN_0.domain, function (error) {
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
if (error) return done(error);
done();
});
@@ -728,7 +726,7 @@ describe('Mail API', function () {
describe('mailinglists', function () {
after(function (done) {
mail._removeMailboxes(DOMAIN_0.domain, function (error) {
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
if (error) return done(error);
done();
+90 -19
View File
@@ -58,9 +58,9 @@ describe('Settings API', function () {
before(setup);
after(cleanup);
describe('autoupdate_pattern', function () {
describe('app_autoupdate_pattern', function () {
it('can get app auto update pattern (default)', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
superagent.get(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -69,8 +69,8 @@ describe('Settings API', function () {
});
});
it('cannot set autoupdate_pattern without pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
it('cannot set app_autoupdate_pattern without pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
@@ -78,8 +78,8 @@ describe('Settings API', function () {
});
});
it('can set autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
it('can set app_autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '00 30 11 * * 1-5' })
.end(function (err, res) {
@@ -88,8 +88,8 @@ describe('Settings API', function () {
});
});
it('can get auto update pattern', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
it('can get app auto update pattern', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -98,8 +98,8 @@ describe('Settings API', function () {
});
});
it('can set autoupdate_pattern to never', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
it('can set app_autoupdate_pattern to never', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: constants.AUTOUPDATE_PATTERN_NEVER })
.end(function (err, res) {
@@ -108,8 +108,8 @@ describe('Settings API', function () {
});
});
it('can get auto update pattern', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
it('can get app auto update pattern', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -118,8 +118,79 @@ describe('Settings API', function () {
});
});
it('cannot set invalid autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
it('cannot set invalid app_autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '1 3 x 5 6' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
});
describe('box_autoupdate_pattern', function () {
it('can get app auto update pattern (default)', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.pattern).to.be.ok();
done();
});
});
it('cannot set box_autoupdate_pattern without pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('can set box_autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '00 30 11 * * 1-5' })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
it('can get app auto update pattern', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.pattern).to.be('00 30 11 * * 1-5');
done();
});
});
it('can set box_autoupdate_pattern to never', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: constants.AUTOUPDATE_PATTERN_NEVER })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
it('can get app auto update pattern', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.pattern).to.be(constants.AUTOUPDATE_PATTERN_NEVER);
done();
});
});
it('cannot set invalid box_autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '1 3 x 5 6' })
.end(function (err, res) {
@@ -149,7 +220,7 @@ describe('Settings API', function () {
format: 'tgz',
encryption: null,
retentionPolicy: { keepWithinSecs: 2 * 24 * 60 * 60 }, // 2 days
schedulePattern: '00 00 23 * * *' // every day at 11pm
intervalSecs: 24 * 60 * 60 // ~1 day
};
it('can get backup_config (default)', function (done) {
@@ -188,9 +259,9 @@ describe('Settings API', function () {
});
});
it('cannot set backup_config without schedulePattern', function (done) {
it('cannot set backup_config without intervalSecs', function (done) {
var tmp = JSON.parse(JSON.stringify(defaultConfig));
delete tmp.schedulePattern;
delete tmp.intervalSecs;
superagent.post(SERVER_URL + '/api/v1/settings/backup_config')
.query({ access_token: token })
@@ -201,9 +272,9 @@ describe('Settings API', function () {
});
});
it('cannot set backup_config with invalid schedulePattern', function (done) {
it('cannot set backup_config with invalid intervalSecs', function (done) {
var tmp = JSON.parse(JSON.stringify(defaultConfig));
tmp.schedulePattern = 'not a pattern';
tmp.intervalSecs = 'not a number';
superagent.post(SERVER_URL + '/api/v1/settings/backup_config')
.query({ access_token: token })
+1 -1
View File
@@ -117,7 +117,7 @@ describe('Tasks API', function () {
expect(res.body.active).to.be(false); // finished
expect(res.body.success).to.be(false);
expect(res.body.result).to.be(null);
expect(res.body.error.message).to.contain('stopped');
expect(res.body.error.message).to.contain('signal SIGTERM');
done();
});
});
+96 -93
View File
@@ -7,80 +7,71 @@ exports = module.exports = {
let apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:scheduler'),
docker = require('./docker.js'),
_ = require('underscore');
// appId -> { containerId, schedulerConfig (manifest), cronjobs }
// appId -> { schedulerConfig (manifest), cronjobs }
var gState = { };
function sync() {
apps.getAll(function (error, allApps) {
if (error) return debug(`sync: error getting app list. ${error.message}`);
function runTask(appId, taskName, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskName, 'string');
assert.strictEqual(typeof callback, 'function');
var allAppIds = allApps.map(function (app) { return app.id; });
var removedAppIds = _.difference(Object.keys(gState), allAppIds);
if (removedAppIds.length !== 0) debug(`sync: stopping jobs of removed apps ${JSON.stringify(removedAppIds)}`);
const JOB_MAX_TIME = 30 * 60 * 1000; // 30 minutes
const containerName = `${appId}-${taskName}`;
async.eachSeries(removedAppIds, function (appId, iteratorDone) {
stopJobs(appId, gState[appId], iteratorDone);
}, function (error) {
if (error) debug(`sync: error stopping jobs of removed apps: ${error.message}`);
apps.get(appId, function (error, app) {
if (error) return callback(error);
gState = _.omit(gState, removedAppIds);
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING || app.health !== apps.HEALTH_HEALTHY) return callback();
async.eachSeries(allApps, function (app, iteratorDone) {
var appState = gState[app.id] || null;
var schedulerConfig = app.manifest.addons ? app.manifest.addons.scheduler : null;
docker.inspectByName(containerName, function (error, data) {
if (!error && data && data.State.Running === true) {
const jobStartTime = new Date(data.State.StartedAt); // iso 8601
if (new Date() - jobStartTime < JOB_MAX_TIME) return callback();
}
if (!appState && !schedulerConfig) return iteratorDone(); // nothing changed
docker.restartContainer(containerName, callback);
if (appState && _.isEqual(appState.schedulerConfig, schedulerConfig) && appState.cronJobs) {
return iteratorDone(); // nothing changed
}
stopJobs(app.id, appState, function (error) {
if (error) debug(`sync: error stopping jobs of ${app.id} : ${error.message}`);
if (!schedulerConfig) {
delete gState[app.id];
return iteratorDone();
}
gState[app.id] = {
schedulerConfig: schedulerConfig,
cronJobs: createCronJobs(app, schedulerConfig)
};
iteratorDone();
});
});
});
});
}
function createJobs(app, schedulerConfig, callback) {
assert.strictEqual(typeof app, 'object');
assert(schedulerConfig && typeof schedulerConfig === 'object');
function killContainer(containerName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof callback, 'function');
const appId = app.id;
let jobs = { };
async.series([
docker.stopContainerByName.bind(null, containerName),
docker.deleteContainerByName.bind(null, containerName)
], function (error) {
if (error) debug(`killContainer: failed to kill task with name ${containerName} : ${error.message}`);
async.eachSeries(Object.keys(schedulerConfig), function (taskName, iteratorDone) {
const task = schedulerConfig[taskName];
const randomSecond = Math.floor(60*Math.random()); // don't start all crons to decrease memory pressure
const cronTime = (constants.TEST ? '*/5 ' : `${randomSecond} `) + task.schedule; // time ticks faster in tests
const containerName = `${app.id}-${taskName}`;
const cmd = schedulerConfig[taskName].command;
// stopJobs only deletes jobs since previous run. This means that when box code restarts, none of the containers
// are removed. The deleteContainer here ensures we re-create the cron containers with the latest config
docker.deleteContainer(containerName, function ( /* ignoredError */) {
docker.createSubcontainer(app, containerName, [ '/bin/sh', '-c', cmd ], { } /* options */, function (error, container) {
if (error && error.reason !== BoxError.ALREADY_EXISTS) return iteratorDone(error);
debug(`createJobs: ${taskName} (${app.fqdn}) will run in container ${container.id}`);
var cronJob = new CronJob({
cronTime: cronTime, // at this point, the pattern has been validated
onTick: () => runTask(appId, taskName, (error) => { // put the app id in closure, so we don't use the outdated app object by mistake
if (error) debug(`could not run task ${taskName} : ${error.message}`);
}),
start: true
});
jobs[taskName] = cronJob;
iteratorDone();
});
});
}, function (error) {
callback(error, jobs);
callback(error);
});
}
@@ -92,62 +83,74 @@ function stopJobs(appId, appState, callback) {
if (!appState) return callback();
async.eachSeries(Object.keys(appState.schedulerConfig), function (taskName, iteratorDone) {
if (appState.cronJobs && appState.cronJobs[taskName]) appState.cronJobs[taskName].stop();
if (appState.cronJobs && appState.cronJobs[taskName]) { // could be null across restarts
appState.cronJobs[taskName].stop();
}
const containerName = `${appId}-${taskName}`;
docker.deleteContainer(containerName, function (error) {
if (error) debug(`stopJobs: failed to delete task container with name ${containerName} : ${error.message}`);
iteratorDone();
});
killContainer(`${appId}-${taskName}`, iteratorDone);
}, callback);
}
function sync() {
apps.getAll(function (error, allApps) {
if (error) return debug(`sync: error getting app list. ${error.message}`);
function createCronJobs(app, schedulerConfig) {
assert.strictEqual(typeof app, 'object');
assert(schedulerConfig && typeof schedulerConfig === 'object');
var allAppIds = allApps.map(function (app) { return app.id; });
var removedAppIds = _.difference(Object.keys(gState), allAppIds);
if (removedAppIds.length !== 0) debug(`sync: stopping jobs of removed apps ${JSON.stringify(removedAppIds)}`);
const appId = app.id;
var jobs = { };
async.eachSeries(removedAppIds, function (appId, iteratorDone) {
debug(`sync: removing jobs of ${appId}`);
stopJobs(appId, gState[appId], iteratorDone);
}, function (error) {
if (error) debug(`sync: error stopping jobs of removed apps: ${error.message}`);
Object.keys(schedulerConfig).forEach(function (taskName) {
var task = schedulerConfig[taskName];
gState = _.omit(gState, removedAppIds);
const randomSecond = Math.floor(60*Math.random()); // don't start all crons to decrease memory pressure
async.eachSeries(allApps, function (app, iteratorDone) {
var appState = gState[app.id] || null;
var schedulerConfig = app.manifest.addons ? app.manifest.addons.scheduler : null;
var cronTime = (constants.TEST ? '*/5 ' : `${randomSecond} `) + task.schedule; // time ticks faster in tests
if (!appState && !schedulerConfig) return iteratorDone(); // nothing to do
if (appState && appState.cronJobs) { // we had created jobs for this app previously
if (_.isEqual(appState.schedulerConfig, schedulerConfig) && appState.containerId === app.containerId) return iteratorDone(); // nothing changed
}
var cronJob = new CronJob({
cronTime: cronTime, // at this point, the pattern has been validated
onTick: () => runTask(appId, taskName, (error) => { // put the app id in closure, so we don't use the outdated app object by mistake
if (error) debug(`could not run task ${taskName} : ${error.message}`);
}),
start: true
});
debug(`sync: adding jobs of ${app.id} (${app.fqdn})`);
jobs[taskName] = cronJob;
});
stopJobs(app.id, appState, function (error) {
if (error) debug(`sync: error stopping jobs of ${app.id} : ${error.message}`);
return jobs;
}
if (!schedulerConfig) { // updated app version removed scheduler addon
delete gState[app.id];
return iteratorDone();
}
function runTask(appId, taskName, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskName, 'string');
assert.strictEqual(typeof callback, 'function');
createJobs(app, schedulerConfig, function (error, cronJobs) {
if (error) return iteratorDone(error); // if docker is down, the next sync() will recreate everything for this app
const JOB_MAX_TIME = 30 * 60 * 1000; // 30 minutes
gState[app.id] = { containerId: app.containerId, schedulerConfig, cronJobs };
apps.get(appId, function (error, app) {
if (error) return callback(error);
iteratorDone();
});
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING || app.health !== apps.HEALTH_HEALTHY) {
return callback();
}
const containerName = `${app.id}-${taskName}`;
docker.inspectByName(containerName, function (err, data) {
if (!err && data && data.State.Running === true) {
const jobStartTime = new Date(data.State.StartedAt); // iso 8601
if (new Date() - jobStartTime < JOB_MAX_TIME) return callback();
}
killContainer(containerName, function (error) {
if (error) return callback(error);
const cmd = gState[appId].schedulerConfig[taskName].command;
// NOTE: if you change container name here, fix addons.js to return correct container names
docker.createSubcontainer(app, containerName, [ '/bin/sh', '-c', cmd ], { } /* options */, function (error, container) {
if (error) return callback(error);
docker.startContainer(container.id, callback);
});
}, function (error) {
if (error) return debug('sync: error creating jobs', error.message);
});
});
});
+2
View File
@@ -7,6 +7,8 @@
if (process.argv[2] === '--check') return console.log('OK');
require('supererror')({ splatchError: true });
var assert = require('assert'),
async = require('async'),
backups = require('../backups.js'),
+1 -1
View File
@@ -37,4 +37,4 @@ if [[ "${cmd}" == "clear" ]]; then
else
# this make not succeed if volume is a mount point
rmdir "${volume_dir}" || true
fi
fi
+1
View File
@@ -13,6 +13,7 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
fi
if [[ "${BOX_ENV}" == "cloudron" ]]; then
sync
shutdown -r now
fi
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
mailbox="$1"
if [[ "${BOX_ENV}" == "cloudron" ]]; then
readonly mailbox_dir="${HOME}/boxdata/mail/vmail/$1"
else
readonly mailbox_dir="${HOME}/.cloudron_test/boxdata/mail/vmail/$1"
fi
rm -rf "${mailbox_dir}"
-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# == 1 && "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
ipset flush cloudron_blocklist
user_firewall_json="/home/yellowtent/boxdata/firewall/blocklist.txt"
if [[ -f "${user_firewall_json}" ]]; then
# without the -n block, any last line without a new line won't be read it!
while read -r line || [[ -n "$line" ]]; do
[[ -z "${line}" ]] && continue # ignore empty lines
[[ "$line" =~ ^#.*$ ]] && continue # ignore lines starting with #
ipset add -! cloudron_blocklist "${line}" # the -! ignore duplicates
done < "${user_firewall_json}"
fi
-70
View File
@@ -1,70 +0,0 @@
#!/bin/bash
set -eu -o pipefail
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly task_worker="${script_dir}/../taskworker.js"
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
readonly task_id="$1"
readonly logfile="$2"
readonly nice="$3"
readonly memory_limit_mb="$4"
readonly service_name="box-task-${task_id}"
systemctl reset-failed "${service_name}" 2>/dev/null || true
readonly id=$(id -u $SUDO_USER)
readonly ubuntu_version=$(lsb_release -rs)
if [[ "${ubuntu_version}" == "16.04" ]]; then
options="-p MemoryLimit=${memory_limit_mb}M --remain-after-exit"
else
options="-p MemoryMax=${memory_limit_mb}M --pipe --wait"
# Note: BindsTo will kill this task when the box is stopped. but will not kill this task when restarted!
# For this reason, we have code to kill the tasks both on shutdown and startup.
# BindsTo does not work on ubuntu 16, this means that even if box is stopped, the tasks keep running
[[ "$BOX_ENV" == "cloudron" ]] && options="${options} -p BindsTo=box.service"
fi
# systemd 237 on ubuntu 18.04 does not apply --nice
if [[ "${ubuntu_version}" == "18.04" ]]; then
(sleep 1; pid=$(systemctl show "${service_name}" -p MainPID | sed 's/MainPID=//g'); renice -n ${nice} -g ${pid} || true) &
fi
# DEBUG has to be hardcoded because it is not set in the tests. --setenv is required for ubuntu 16 (-E does not work)
systemd-run --unit "${service_name}" --nice "${nice}" --uid=${id} --gid=${id} ${options} \
--setenv HOME=${HOME} --setenv USER=${SUDO_USER} --setenv DEBUG=box:* --setenv BOX_ENV=${BOX_ENV} --setenv NODE_ENV=production \
"${task_worker}" "${task_id}" "${logfile}"
exit_code=$?
if [[ "${ubuntu_version}" == "16.04" ]]; then
sleep 3
# we cannot use systemctl is-active because unit is always active until stopped with RemainAfterExit
while [[ "$(systemctl show -p SubState ${service_name})" == *"running"* ]]; do
echo "Waiting for service ${service_name} to finish"
sleep 3
done
exit_code=$(systemctl show "${service_name}" -p ExecMainStatus | sed 's/ExecMainStatus=//g')
systemctl stop "${service_name}" || true # because of remain-after-exit we have to deactivate the service
fi
[[ "${ubuntu_version}" == "18.04" ]] && wait # for the renice subshell we started
echo "Service ${service_name} finished with exit code ${exit_code}"
exit "${exit_code}"
-31
View File
@@ -1,31 +0,0 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
task_id="$1"
if [[ "${task_id}" == "all" ]]; then
systemctl list-units --full --no-legend box-task-* # just to show who was running
systemctl kill --signal=SIGTERM box-task-* || true
systemctl reset-failed box-task-* || true
systemctl stop box-task-* || true # because of remain-after-exit in Ubuntu 16 we have to deactivate the service
else
readonly service_name="box-task-${task_id}"
systemctl kill --signal=SIGTERM "${service_name}" || true
systemctl stop "${service_name}" || true # because of remain-after-exit in Ubuntu 16 we have to deactivate the service
fi
+9 -24
View File
@@ -10,7 +10,6 @@ let assert = require('assert'),
cloudron = require('./cloudron.js'),
constants = require('./constants.js'),
database = require('./database.js'),
debug = require('debug')('box:server'),
eventlog = require('./eventlog.js'),
express = require('express'),
http = require('http'),
@@ -106,7 +105,7 @@ function initializeExpressSync() {
router.get ('/api/v1/cloudron/update', token, authorizeAdmin, routes.cloudron.getUpdateInfo);
router.post('/api/v1/cloudron/update', json, token, authorizeAdmin, routes.cloudron.update);
router.post('/api/v1/cloudron/prepare_dashboard_domain', json, token, authorizeAdmin, routes.cloudron.prepareDashboardDomain);
router.post('/api/v1/cloudron/set_dashboard_domain', json, token, authorizeAdmin, routes.cloudron.updateDashboardDomain);
router.post('/api/v1/cloudron/set_dashboard_domain', json, token, authorizeAdmin, routes.cloudron.setDashboardAndMailDomain);
router.post('/api/v1/cloudron/renew_certs', json, token, authorizeAdmin, routes.cloudron.renewCerts);
router.post('/api/v1/cloudron/check_for_updates', json, token, authorizeAdmin, routes.cloudron.checkForUpdates);
router.get ('/api/v1/cloudron/reboot', token, authorizeAdmin, routes.cloudron.isRebootRequired);
@@ -137,7 +136,6 @@ function initializeExpressSync() {
router.get ('/api/v1/backups', token, authorizeAdmin, routes.backups.list);
router.post('/api/v1/backups/create', token, authorizeAdmin, routes.backups.startBackup);
router.post('/api/v1/backups/cleanup', json, token, authorizeAdmin, routes.backups.cleanup);
router.get ('/api/v1/backups/check', token, authorizeAdmin, routes.backups.check);
// config route (for dashboard). can return some private configuration unlike status
router.get ('/api/v1/config', token, routes.cloudron.getConfig);
@@ -241,27 +239,19 @@ function initializeExpressSync() {
return req.params.setting === 'cloudron_avatar' ? multipart(req, res, next) : next();
}, routes.branding.set);
// network routes
router.get ('/api/v1/network/blocklist', token, authorizeOwner, routes.network.getBlocklist);
router.post('/api/v1/network/blocklist', json, token, authorizeOwner, routes.network.setBlocklist);
// settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above)
router.get ('/api/v1/settings/:setting', token, authorizeAdmin, routes.settings.get);
router.post('/api/v1/settings/backup_config', json, token, authorizeOwner, routes.settings.setBackupConfig);
router.post('/api/v1/settings/:setting', json, token, authorizeAdmin, routes.settings.set);
// email routes
router.get ('/api/v1/mailserver/eventlog', token, authorizeOwner, routes.mailserver.proxy);
router.get ('/api/v1/mailserver/usage', token, authorizeAdmin, routes.mailserver.proxy);
router.post('/api/v1/mailserver/clear_eventlog', token, authorizeOwner, routes.mailserver.proxy);
router.get ('/api/v1/mailserver/location', token, authorizeAdmin, routes.mailserver.getLocation);
router.post('/api/v1/mailserver/location', json, token, authorizeAdmin, routes.mailserver.setLocation);
router.get ('/api/v1/mailserver/max_email_size', token, authorizeAdmin, routes.mailserver.proxy);
router.post('/api/v1/mailserver/max_email_size', token, authorizeAdmin, routes.mailserver.proxy);
router.get ('/api/v1/mailserver/spam_acl', token, authorizeAdmin, routes.mailserver.proxy);
router.post('/api/v1/mailserver/spam_acl', token, authorizeAdmin, routes.mailserver.proxy);
router.get ('/api/v1/mailserver/spam_custom_config', token, authorizeAdmin, routes.mailserver.proxy);
router.post('/api/v1/mailserver/spam_custom_config', token, authorizeAdmin, routes.mailserver.proxy);
router.get('/api/v1/mailserver/:pathname', token, (req, res, next) => {
// some routes are more special than others
if (req.params.pathname === 'eventlog' || req.params.pathname === 'clear_eventlog') {
return authorizeOwner(req, res, next);
}
authorizeAdmin(req, res, next);
}, routes.mailserver.proxy);
router.get ('/api/v1/mail/:domain', token, authorizeAdmin, routes.mail.getDomain);
router.get ('/api/v1/mail/:domain/status', token, authorizeAdmin, routes.mail.getStatus);
@@ -270,14 +260,13 @@ function initializeExpressSync() {
router.post('/api/v1/mail/:domain/relay', json, token, authorizeAdmin, routes.mail.setMailRelay);
router.post('/api/v1/mail/:domain/enable', json, token, authorizeAdmin, routes.mail.setMailEnabled);
router.post('/api/v1/mail/:domain/dns', json, token, authorizeAdmin, routes.mail.setDnsRecords);
router.post('/api/v1/mail/:domain/banner', json, token, authorizeAdmin, routes.mail.setBanner);
router.post('/api/v1/mail/:domain/send_test_mail', json, token, authorizeAdmin, routes.mail.sendTestMail);
router.get ('/api/v1/mail/:domain/mailbox_count', token, authorizeAdmin, routes.mail.getMailboxCount);
router.get ('/api/v1/mail/:domain/mailboxes', token, authorizeAdmin, routes.mail.listMailboxes);
router.get ('/api/v1/mail/:domain/mailboxes/:name', token, authorizeAdmin, routes.mail.getMailbox);
router.post('/api/v1/mail/:domain/mailboxes', json, token, authorizeAdmin, routes.mail.addMailbox);
router.post('/api/v1/mail/:domain/mailboxes/:name', json, token, authorizeAdmin, routes.mail.updateMailbox);
router.del ('/api/v1/mail/:domain/mailboxes/:name', json, token, authorizeAdmin, routes.mail.removeMailbox);
router.del ('/api/v1/mail/:domain/mailboxes/:name', token, authorizeAdmin, routes.mail.removeMailbox);
router.get ('/api/v1/mail/:domain/mailboxes/:name/aliases', token, authorizeAdmin, routes.mail.getAliases);
router.put ('/api/v1/mail/:domain/mailboxes/:name/aliases', json, token, authorizeAdmin, routes.mail.setAliases);
router.get ('/api/v1/mail/:domain/lists', token, authorizeAdmin, routes.mail.getLists);
@@ -342,10 +331,6 @@ function start(callback) {
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(gHttpServer, null, 'Server is already up and running.');
debug('==========================================');
debug(` Cloudron ${constants.VERSION} `);
debug('==========================================');
gHttpServer = initializeExpressSync();
async.series([
+101 -106
View File
@@ -1,77 +1,80 @@
'use strict';
exports = module.exports = {
getAutoupdatePattern,
setAutoupdatePattern,
getAppAutoupdatePattern: getAppAutoupdatePattern,
setAppAutoupdatePattern: setAppAutoupdatePattern,
getTimeZone,
setTimeZone,
getBoxAutoupdatePattern: getBoxAutoupdatePattern,
setBoxAutoupdatePattern: setBoxAutoupdatePattern,
getCloudronName,
setCloudronName,
getTimeZone: getTimeZone,
setTimeZone: setTimeZone,
getCloudronAvatar,
setCloudronAvatar,
getCloudronName: getCloudronName,
setCloudronName: setCloudronName,
getDynamicDnsConfig,
setDynamicDnsConfig,
getCloudronAvatar: getCloudronAvatar,
setCloudronAvatar: setCloudronAvatar,
getUnstableAppsConfig,
setUnstableAppsConfig,
getDynamicDnsConfig: getDynamicDnsConfig,
setDynamicDnsConfig: setDynamicDnsConfig,
getBackupConfig,
setBackupConfig,
setBackupCredentials,
getUnstableAppsConfig: getUnstableAppsConfig,
setUnstableAppsConfig: setUnstableAppsConfig,
getPlatformConfig,
setPlatformConfig,
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
setBackupCredentials: setBackupCredentials,
getExternalLdapConfig,
setExternalLdapConfig,
getPlatformConfig: getPlatformConfig,
setPlatformConfig: setPlatformConfig,
getRegistryConfig,
setRegistryConfig,
getExternalLdapConfig: getExternalLdapConfig,
setExternalLdapConfig: setExternalLdapConfig,
getLicenseKey,
setLicenseKey,
getRegistryConfig: getRegistryConfig,
setRegistryConfig: setRegistryConfig,
getCloudronId,
setCloudronId,
getLicenseKey: getLicenseKey,
setLicenseKey: setLicenseKey,
getCloudronToken,
setCloudronToken,
getCloudronId: getCloudronId,
setCloudronId: setCloudronId,
getSysinfoConfig,
setSysinfoConfig,
getCloudronToken: getCloudronToken,
setCloudronToken: setCloudronToken,
getFooter,
setFooter,
getSysinfoConfig: getSysinfoConfig,
setSysinfoConfig: setSysinfoConfig,
getDirectoryConfig,
setDirectoryConfig,
getFooter: getFooter,
setFooter: setFooter,
getAppstoreListingConfig,
setAppstoreListingConfig,
getDirectoryConfig: getDirectoryConfig,
setDirectoryConfig: setDirectoryConfig,
getSupportConfig,
provider,
getAll,
initCache,
getAppstoreListingConfig: getAppstoreListingConfig,
setAppstoreListingConfig: setAppstoreListingConfig,
getSupportConfig: getSupportConfig,
provider: provider,
getAll: getAll,
initCache: initCache,
// these values come from the cache
apiServerOrigin,
webServerOrigin,
adminDomain,
setAdminLocation,
setMailLocation,
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
adminDomain: adminDomain,
setAdmin: setAdmin,
// these values are derived
adminOrigin: adminOrigin,
adminFqdn: adminFqdn,
mailFqdn: mailFqdn,
mailFqdn,
mailDomain,
adminOrigin,
adminFqdn,
isDemo,
isDemo: isDemo,
// booleans. if you add an entry here, be sure to fix getAll
DYNAMIC_DNS_KEY: 'dynamic_dns',
@@ -89,7 +92,8 @@ exports = module.exports = {
DIRECTORY_CONFIG_KEY: 'directory_config',
// strings
AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern',
APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern',
BOX_AUTOUPDATE_PATTERN_KEY: 'box_autoupdate_pattern',
TIME_ZONE_KEY: 'time_zone',
CLOUDRON_NAME_KEY: 'cloudron_name',
LICENSE_KEY: 'license_key',
@@ -100,8 +104,6 @@ exports = module.exports = {
WEB_SERVER_ORIGIN_KEY: 'web_server_origin',
ADMIN_DOMAIN_KEY: 'admin_domain',
ADMIN_FQDN_KEY: 'admin_fqdn',
MAIL_DOMAIN_KEY: 'mail_domain',
MAIL_FQDN_KEY: 'mail_fqdn',
PROVIDER_KEY: 'provider',
FOOTER_KEY: 'footer',
@@ -113,8 +115,6 @@ exports = module.exports = {
_setApiServerOrigin: setApiServerOrigin
};
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
var addons = require('./addons.js'),
assert = require('assert'),
backups = require('./backups.js'),
@@ -135,7 +135,8 @@ var addons = require('./addons.js'),
let gDefaults = (function () {
var result = { };
result[exports.AUTOUPDATE_PATTERN_KEY] = cron.DEFAULT_AUTOUPDATE_PATTERN;
result[exports.APP_AUTOUPDATE_PATTERN_KEY] = cron.DEFAULT_APP_AUTOUPDATE_PATTERN;
result[exports.BOX_AUTOUPDATE_PATTERN_KEY] = cron.DEFAULT_BOX_AUTOUPDATE_PATTERN;
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DYNAMIC_DNS_KEY] = false;
@@ -149,7 +150,7 @@ let gDefaults = (function () {
format: 'tgz',
encryption: null,
retentionPolicy: { keepWithinSecs: 2 * 24 * 60 * 60 }, // 2 days
schedulePattern: '00 00 23 * * *' // every day at 11pm
intervalSecs: 24 * 60 * 60 // ~1 day
};
result[exports.PLATFORM_CONFIG_KEY] = {};
result[exports.EXTERNAL_LDAP_KEY] = {
@@ -167,9 +168,6 @@ let gDefaults = (function () {
result[exports.ADMIN_DOMAIN_KEY] = '';
result[exports.ADMIN_FQDN_KEY] = '';
result[exports.MAIL_DOMAIN_KEY] = '';
result[exports.MAIL_FQDN_KEY] = '';
result[exports.API_SERVER_ORIGIN_KEY] = 'https://api.cloudron.io';
result[exports.WEB_SERVER_ORIGIN_KEY] = 'https://cloudron.io';
result[exports.DEMO_KEY] = false;
@@ -184,8 +182,8 @@ let gDefaults = (function () {
remoteSupport: true,
ticketFormBody:
'Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).\n\n'
+ '* [Knowledge Base & App Docs](https://docs.cloudron.io/apps/?support_view)\n'
+ '* [Custom App Packaging & API](https://docs.cloudron.io/custom-apps/tutorial/?support_view)\n'
+ '* [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)\n'
+ '* [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)\n'
+ '* [Forum](https://forum.cloudron.io/)\n\n',
submitTickets: true
};
@@ -203,7 +201,7 @@ function notifyChange(key, value) {
cron.handleSettingsChanged(key, value);
}
function setAutoupdatePattern(pattern, callback) {
function setAppAutoupdatePattern(pattern, callback) {
assert.strictEqual(typeof pattern, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -212,20 +210,49 @@ function setAutoupdatePattern(pattern, callback) {
if (!job) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid pattern', { field: 'pattern' }));
}
settingsdb.set(exports.AUTOUPDATE_PATTERN_KEY, pattern, function (error) {
settingsdb.set(exports.APP_AUTOUPDATE_PATTERN_KEY, pattern, function (error) {
if (error) return callback(error);
notifyChange(exports.AUTOUPDATE_PATTERN_KEY, pattern);
notifyChange(exports.APP_AUTOUPDATE_PATTERN_KEY, pattern);
return callback(null);
});
}
function getAutoupdatePattern(callback) {
function getAppAutoupdatePattern(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.AUTOUPDATE_PATTERN_KEY, function (error, pattern) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.AUTOUPDATE_PATTERN_KEY]);
settingsdb.get(exports.APP_AUTOUPDATE_PATTERN_KEY, function (error, pattern) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.APP_AUTOUPDATE_PATTERN_KEY]);
if (error) return callback(error);
callback(null, pattern);
});
}
function setBoxAutoupdatePattern(pattern, callback) {
assert.strictEqual(typeof pattern, 'string');
assert.strictEqual(typeof callback, 'function');
if (pattern !== constants.AUTOUPDATE_PATTERN_NEVER) { // check if pattern is valid
var job = safe.safeCall(function () { return new CronJob(pattern); });
if (!job) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid pattern', { field: 'pattern' }));
}
settingsdb.set(exports.BOX_AUTOUPDATE_PATTERN_KEY, pattern, function (error) {
if (error) return callback(error);
notifyChange(exports.BOX_AUTOUPDATE_PATTERN_KEY, pattern);
return callback(null);
});
}
function getBoxAutoupdatePattern(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.BOX_AUTOUPDATE_PATTERN_KEY, function (error, pattern) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.BOX_AUTOUPDATE_PATTERN_KEY]);
if (error) return callback(error);
callback(null, pattern);
@@ -389,11 +416,7 @@ function setBackupConfig(backupConfig, callback) {
delete backupConfig.password;
}
// if any of these changes, we have to clear the cache
if ([ 'format', 'provider', 'prefix', 'bucket', 'region', 'endpoint', 'backupFolder', 'mountPoint', 'encryption' ].some(p => backupConfig[p] !== currentConfig[p])) {
debug('setBackupConfig: clearing backup cache');
backups.cleanupCacheFilesSync();
}
backups.cleanupCacheFilesSync();
settingsdb.set(exports.BACKUP_CONFIG_KEY, JSON.stringify(backupConfig), function (error) {
if (error) return callback(error);
@@ -414,7 +437,7 @@ function setBackupCredentials(credentials, callback) {
if (error) return callback(error);
// preserve these fields
const extra = _.pick(currentConfig, 'retentionPolicy', 'schedulePattern', 'copyConcurrency', 'syncConcurrency', 'memoryLimit', 'downloadConcurrency', 'deleteConcurrency', 'uploadPartSize');
const extra = _.pick(currentConfig, 'retentionPolicy', 'intervalSecs', 'copyConcurrency', 'syncConcurrency');
const backupConfig = _.extend({}, credentials, extra);
@@ -452,9 +475,7 @@ function setPlatformConfig(platformConfig, callback) {
settingsdb.set(exports.PLATFORM_CONFIG_KEY, JSON.stringify(platformConfig), function (error) {
if (error) return callback(error);
callback(null); // updating service config can take a while
addons.updateServiceConfig(platformConfig, NOOP_CALLBACK);
addons.updateServiceConfig(platformConfig, callback);
});
}
@@ -478,8 +499,6 @@ function setExternalLdapConfig(externalLdapConfig, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (isDemo()) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'));
getExternalLdapConfig(function (error, currentConfig) {
if (error) return callback(error);
@@ -578,8 +597,6 @@ function setDirectoryConfig(directoryConfig, callback) {
assert.strictEqual(typeof directoryConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (isDemo()) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'));
settingsdb.set(exports.DIRECTORY_CONFIG_KEY, JSON.stringify(directoryConfig), function (error) {
if (error) return callback(error);
@@ -733,8 +750,6 @@ function initCache(callback) {
webServerOrigin: allSettings[exports.WEB_SERVER_ORIGIN_KEY],
adminDomain: allSettings[exports.ADMIN_DOMAIN_KEY],
adminFqdn: allSettings[exports.ADMIN_FQDN_KEY],
mailDomain: allSettings[exports.MAIL_DOMAIN_KEY],
mailFqdn: allSettings[exports.MAIL_FQDN_KEY],
isDemo: allSettings[exports.DEMO_KEY],
provider: provider ? provider.trim() : 'generic'
};
@@ -744,7 +759,7 @@ function initCache(callback) {
}
// this is together so we can do this in a transaction later
function setAdminLocation(adminDomain, adminFqdn, callback) {
function setAdmin(adminDomain, adminFqdn, callback) {
assert.strictEqual(typeof adminDomain, 'string');
assert.strictEqual(typeof adminFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -763,25 +778,6 @@ function setAdminLocation(adminDomain, adminFqdn, callback) {
});
}
function setMailLocation(mailDomain, mailFqdn, callback) {
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.MAIL_DOMAIN_KEY, mailDomain, function (error) {
if (error) return callback(error);
settingsdb.set(exports.MAIL_FQDN_KEY, mailFqdn, function (error) {
if (error) return callback(error);
gCache.mailDomain = mailDomain;
gCache.mailFqdn = mailFqdn;
callback(null);
});
});
}
function setApiServerOrigin(origin, callback) {
assert.strictEqual(typeof origin, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -826,6 +822,5 @@ function webServerOrigin() { return gCache.webServerOrigin; }
function adminDomain() { return gCache.adminDomain; }
function adminFqdn() { return gCache.adminFqdn; }
function isDemo() { return gCache.isDemo; }
function mailDomain() { return gCache.mailDomain; }
function mailFqdn() { return gCache.mailFqdn; }
function mailFqdn() { return adminFqdn(); }
function adminOrigin() { return 'https://' + adminFqdn(); }
+23 -92
View File
@@ -1,109 +1,40 @@
'use strict';
exports = module.exports = {
startSftp: startSftp,
rebuild: rebuild
startSftp: startSftp
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
debug = require('debug')('box:sftp'),
var assert = require('assert'),
infra = require('./infra_version.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
_ = require('underscore');
paths = require('./paths.js'),
shell = require('./shell.js');
function startSftp(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
if (existingInfra.version === infra.version && infra.images.sftp.tag === existingInfra.images.sftp.tag) return callback();
rebuild(callback);
}
var rebuildInProgress = false;
function rebuild(callback) {
assert.strictEqual(typeof callback, 'function');
if (rebuildInProgress) {
debug('waiting for other rebuild to finish');
return setTimeout(function () { rebuild(callback); }, 5000);
}
rebuildInProgress = true;
function done(error) {
rebuildInProgress = false;
callback(error);
}
debug('rebuilding container');
const tag = infra.images.sftp.tag;
const memoryLimit = 256;
apps.getAll(function (error, result) {
if (error) return done(error);
if (existingInfra.version === infra.version && infra.images.sftp.tag === existingInfra.images.sftp.tag) return callback();
let dataDirs = [];
result.forEach(function (app) {
if (!app.manifest.addons['localstorage']) return;
const cmd = `docker run --restart=always -d --name="sftp" \
--hostname sftp \
--net cloudron \
--net-alias sftp \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=sftp \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-p 222:22 \
-v "${paths.APPS_DATA_DIR}:/app/data" \
-v "/etc/ssh:/etc/ssh:ro" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
const hostDir = apps.getDataDir(app, app.dataDir), mountDir = `/app/data/${app.id}`;
if (!safe.fs.existsSync(hostDir)) {
// do not create host path when cloudron is restoring. this will then create dir with root perms making restore logic fail
debug(`Ignoring volume for ${app.id} since it does not exist`);
return;
}
dataDirs.push({ hostDir, mountDir });
});
shell.exec('inspectSftp', 'docker inspect --format="{{json .Mounts }}" sftp', function (error, result) {
if (!error && result) {
let currentDataDirs = safe.JSON.parse(result);
if (currentDataDirs) {
currentDataDirs = currentDataDirs.filter(function (d) { return d.Destination.indexOf('/app/data/') === 0; }).map(function (d) { return { hostDir: d.Source, mountDir: d.Destination }; });
// sort for comparison
currentDataDirs.sort(function (a, b) { return a.hostDir < b.hostDir ? -1 : 1; });
dataDirs.sort(function (a, b) { return a.hostDir < b.hostDir ? -1 : 1; });
if (_.isEqual(currentDataDirs, dataDirs)) {
debug('Skipping rebuild, no changes');
return done();
}
}
}
const appDataVolumes = dataDirs.map(function (v) { return `-v "${v.hostDir}:${v.mountDir}"`; }).join(' ');
const cmd = `docker run --restart=always -d --name="sftp" \
--hostname sftp \
--net cloudron \
--net-alias sftp \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=sftp \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-p 222:22 \
${appDataVolumes} \
-v "/etc/ssh:/etc/ssh:ro" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
// ignore error if container not found (and fail later) so that this code works across restarts
async.series([
shell.exec.bind(null, 'stopSftp', 'docker stop sftp || true'),
shell.exec.bind(null, 'removeSftp', 'docker rm -f sftp || true'),
shell.exec.bind(null, 'startSftp', cmd)
], done);
});
});
shell.exec('startSftp', cmd, callback);
}
+6 -8
View File
@@ -13,7 +13,7 @@ var assert = require('assert'),
once = require('once'),
util = require('util');
const SUDO = '/usr/bin/sudo';
var SUDO = '/usr/bin/sudo';
function exec(tag, cmd, callback) {
assert.strictEqual(typeof tag, 'string');
@@ -23,12 +23,10 @@ function exec(tag, cmd, callback) {
debug(`${tag} exec: ${cmd}`);
child_process.exec(cmd, function (error, stdout, stderr) {
const stdoutResult = stdout.toString('utf8');
debug(`${tag} (stdout): %s`, stdoutResult);
debug(`${tag} (stdout): %s`, stdout.toString('utf8'));
debug(`${tag} (stderr): %s`, stderr.toString('utf8'));
callback(error, stdoutResult);
callback(error);
});
}
@@ -41,11 +39,11 @@ function spawn(tag, file, args, options, callback) {
callback = once(callback); // exit may or may not be called after an 'error'
debug(tag + ' spawn: %s %s', file, args.join(' '));
if (options.ipc) options.stdio = ['pipe', 'pipe', 'pipe', 'ipc'];
debug(tag + ' spawn: %s %s', file, args.join(' '));
const cp = child_process.spawn(file, args, options);
var cp = child_process.spawn(file, args, options);
if (options.logStream) {
cp.stdout.pipe(options.logStream);
cp.stderr.pipe(options.logStream);
+1 -2
View File
@@ -174,8 +174,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
});
}
const batchSize = 1000;
const concurrency = apiConfig.copyConcurrency || 10;
const batchSize = 1000, concurrency = 10;
var total = 0;
listDir(apiConfig, oldFilePath, batchSize, function (entries, done) {
+45 -48
View File
@@ -68,7 +68,7 @@ function getS3Config(apiConfig, callback) {
},
httpOptions: {
connectTimeout: 10000, // https://github.com/aws/aws-sdk-js/pull/1446
timeout: 600 * 1000 // https://github.com/aws/aws-sdk-js/issues/1704 (allow chunk upload to take upto 5 minutes)
timeout: 300 * 1000 // https://github.com/aws/aws-sdk-js/issues/1704 (allow 5MB chunk upload to take upto 5 minutes)
}
};
@@ -118,19 +118,18 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
var s3 = new AWS.S3(credentials);
// s3.upload automatically does a multi-part upload. we set queueSize to 3 to reduce memory usage
// s3.upload automatically does a multi-part upload. we set queueSize to 1 to reduce memory usage
// uploader will buffer at most queueSize * partSize bytes into memory at any given time.
// scaleway only supports 1000 parts per object (https://www.scaleway.com/en/docs/s3-multipart-upload/)
// s3: https://docs.aws.amazon.com/AmazonS3/latest/dev/qfacts.html (max 10k parts and no size limit on the last part!)
const partSize = apiConfig.uploadPartSize || (apiConfig.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024);
const partSize = apiConfig.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024;
s3.upload(params, { partSize, queueSize: 3 }, function (error, data) {
s3.upload(params, { partSize, queueSize: 1 }, function (error, data) {
if (error) {
debug('Error uploading [%s]: s3 upload error.', backupFilePath, error);
return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Error uploading ${backupFilePath}. Message: ${error.message} HTTP Code: ${error.code}`));
}
debug(`Uploaded ${backupFilePath} with partSize ${partSize}: ${JSON.stringify(data)}`);
debug(`Uploaded ${backupFilePath}: ${JSON.stringify(data)}`);
callback(null);
});
@@ -231,7 +230,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
assert.strictEqual(typeof oldFilePath, 'string');
assert.strictEqual(typeof newFilePath, 'string');
var events = new EventEmitter();
var events = new EventEmitter(), retryCount = 0;
function copyFile(entry, iteratorCallback) {
getS3Config(apiConfig, function (error, credentials) {
@@ -263,6 +262,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
copyParams.CopySource = encodeCopySource(apiConfig.bucket, entry.fullPath);
s3.copyObject(copyParams, done).on('retry', function (response) {
++retryCount;
events.emit('progress', `Retrying (${response.retryCount+1}) copy of ${relativePath || oldFilePath}. Error: ${response.error} ${response.httpResponse.statusCode}`);
// on DO, we get a random 408. these are not retried by the SDK
if (response.error) response.error.retryable = true; // https://github.com/aws/aws-sdk-js/issues/412
@@ -273,80 +273,77 @@ function copy(apiConfig, oldFilePath, newFilePath) {
events.emit('progress', `Copying (multipart) ${relativePath || oldFilePath}`);
s3.createMultipartUpload(copyParams, function (error, multipart) {
s3.createMultipartUpload(copyParams, function (error, result) {
if (error) return done(error);
// Exoscale (96M) was suggested by exoscale. 1GB - rather random size for others
const chunkSize = apiConfig.provider === 'exoscale-sos' ? 96 * 1024 * 1024 : 1024 * 1024 * 1024;
const uploadId = multipart.UploadId;
let uploadedParts = [], ranges = [];
var uploadId = result.UploadId;
var uploadedParts = [];
var partNumber = 1;
var startBytes = 0;
var endBytes = 0;
var size = entry.size-1;
let cur = 0;
while (cur + chunkSize < entry.size) {
ranges.push({ startBytes: cur, endBytes: cur + chunkSize - 1 });
cur += chunkSize;
}
ranges.push({ startBytes: cur, endBytes: entry.size-1 });
function copyNextChunk() {
endBytes = startBytes + chunkSize;
if (endBytes > size) endBytes = size;
async.eachOfLimit(ranges, 5, function copyChunk(range, index, iteratorDone) {
const partCopyParams = {
var partCopyParams = {
Bucket: apiConfig.bucket,
Key: path.join(newFilePath, relativePath),
CopySource: encodeCopySource(apiConfig.bucket, entry.fullPath), // See aws-sdk-js/issues/1302
CopySourceRange: 'bytes=' + range.startBytes + '-' + range.endBytes,
PartNumber: index+1,
CopySourceRange: 'bytes=' + startBytes + '-' + endBytes,
PartNumber: partNumber,
UploadId: uploadId
};
events.emit('progress', `Copying part ${partCopyParams.PartNumber} - ${partCopyParams.CopySource} ${partCopyParams.CopySourceRange}`);
s3.uploadPartCopy(partCopyParams, function (error, part) {
if (error) return iteratorDone(error);
s3.uploadPartCopy(partCopyParams, function (error, result) {
if (error) return done(error);
events.emit('progress', `Uploaded part ${partCopyParams.PartNumber} - Etag: ${part.CopyPartResult.ETag}`);
events.emit('progress', `Uploaded part ${partCopyParams.PartNumber} - Etag: ${result.CopyPartResult.ETag}`);
if (!part.CopyPartResult.ETag) return iteratorDone(new Error('Multi-part copy is broken or not implemented by the S3 storage provider'));
if (!result.CopyPartResult.ETag) return done(new Error('Multi-part copy is broken or not implemented by the S3 storage provider'));
uploadedParts[index] = { ETag: part.CopyPartResult.ETag, PartNumber: partCopyParams.PartNumber };
uploadedParts.push({ ETag: result.CopyPartResult.ETag, PartNumber: partNumber });
iteratorDone();
}).on('retry', function (response) {
events.emit('progress', `Retrying (${response.retryCount+1}) multipart copy of ${relativePath || oldFilePath}. Error: ${response.error} ${response.httpResponse.statusCode}`);
});
}, function chunksCopied(error) {
if (error) { // we must still recommend the user to set a AbortIncompleteMultipartUpload lifecycle rule
const abortParams = {
if (endBytes < size) {
startBytes = endBytes + 1;
partNumber++;
return copyNextChunk();
}
var completeMultipartParams = {
Bucket: apiConfig.bucket,
Key: path.join(newFilePath, relativePath),
MultipartUpload: { Parts: uploadedParts },
UploadId: uploadId
};
events.emit('progress', `Aborting multipart copy of ${relativePath || oldFilePath}`);
return s3.abortMultipartUpload(abortParams, () => done(error)); // ignore any abort errors
}
const completeMultipartParams = {
Bucket: apiConfig.bucket,
Key: path.join(newFilePath, relativePath),
MultipartUpload: { Parts: uploadedParts },
UploadId: uploadId
};
events.emit('progress', `Finishing multipart copy - ${completeMultipartParams.Key}`);
events.emit('progress', `Finishing multipart copy - ${completeMultipartParams.Key}`);
s3.completeMultipartUpload(completeMultipartParams, done);
}).on('retry', function (response) {
++retryCount;
events.emit('progress', `Retrying (${response.retryCount+1}) multipart copy of ${relativePath || oldFilePath}. Error: ${response.error} ${response.httpResponse.statusCode}`);
});
}
s3.completeMultipartUpload(completeMultipartParams, done);
});
copyNextChunk();
});
});
}
let total = 0;
var total = 0;
const concurrency = apiConfig.copyConcurrency || (apiConfig.provider === 's3' ? 500 : 10);
events.emit('progress', `Copying with concurrency of ${concurrency}`);
listDir(apiConfig, oldFilePath, 1000, function listDirIterator(entries, done) {
total += entries.length;
events.emit('progress', `Copying files from ${total-entries.length}-${total}`);
events.emit('progress', `Copying ${total-entries.length}-${total}. ${retryCount} errors so far. concurrency set to ${concurrency}`);
retryCount = 0;
async.eachLimit(entries, concurrency, copyFile, done);
}, function (error) {
@@ -399,7 +396,7 @@ function removeDir(apiConfig, pathPrefix) {
listDir(apiConfig, pathPrefix, 1000, function listDirIterator(entries, done) {
total += entries.length;
const chunkSize = apiConfig.deleteConcurrency || (apiConfig.provider !== 'digitalocean-spaces' ? 1000 : 100); // throttle objects in each request
const chunkSize = apiConfig.provider !== 'digitalocean-spaces' ? 1000 : 100; // throttle objects in each request
var chunks = chunk(entries, chunkSize);
async.eachSeries(chunks, function deleteFiles(objects, iteratorCallback) {
+2 -3
View File
@@ -8,7 +8,6 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:sysinfo/generic'),
superagent = require('superagent');
function getServerIp(config, callback) {
@@ -20,11 +19,11 @@ function getServerIp(config, callback) {
async.retry({ times: 10, interval: 5000 }, function (callback) {
superagent.get('https://api.cloudron.io/api/v1/helper/public_ip').timeout(30 * 1000).end(function (error, result) {
if (error || result.statusCode !== 200) {
debug('Error getting IP', error);
console.error('Error getting IP', error);
return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to detect IP. API server unreachable'));
}
if (!result.body && !result.body.ip) {
debug('Unexpected answer. No "ip" found in response body.', result.body);
console.error('Unexpected answer. No "ip" found in response body.', result.body);
return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to detect IP. No IP found in response'));
}
+34 -39
View File
@@ -16,15 +16,14 @@ exports = module.exports = {
removePrivateFields: removePrivateFields,
// task types. if you add a task here, fill up the function table in taskworker and dashboard client.js
// task types. if you add a task here, fill up the function table in taskworker
TASK_APP: 'app',
TASK_BACKUP: 'backup',
TASK_UPDATE: 'update',
TASK_RENEW_CERTS: 'renewcerts',
TASK_SETUP_DNS_AND_CERT: 'setupDnsAndCert',
TASK_PREPARE_DASHBOARD_DOMAIN: 'prepareDashboardDomain',
TASK_CLEAN_BACKUPS: 'cleanBackups',
TASK_SYNC_EXTERNAL_LDAP: 'syncExternalLdap',
TASK_CHANGE_MAIL_LOCATION: 'changeMailLocation',
// error codes
ESTOPPED: 'stopped',
@@ -39,11 +38,12 @@ exports = module.exports = {
};
let assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
child_process = require('child_process'),
debug = require('debug')('box:tasks'),
path = require('path'),
paths = require('./paths.js'),
shell = require('./shell.js'),
safe = require('safetydance'),
spawn = require('child_process').spawn,
split = require('split'),
taskdb = require('./taskdb.js'),
@@ -52,20 +52,14 @@ let assert = require('assert'),
let gTasks = {}; // indexed by task id
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const START_TASK_CMD = path.join(__dirname, 'scripts/starttask.sh');
const STOP_TASK_CMD = path.join(__dirname, 'scripts/stoptask.sh');
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
result.active = !!gTasks[result.id];
// we rely on 'percent' to determine success. maybe this can become a db field
result.success = result.percent === 100 && !result.error;
// we rely on 'percent' to determine pending. maybe this can become a db field
result.pending = result.percent === 1;
// the error in db will be empty if we didn't get a chance to handle task exit
if (!result.active && result.percent !== 100 && !result.error) {
result.error = { message: 'Cloudron crashed/stopped', code: exports.ECRASHED };
@@ -131,61 +125,63 @@ function add(type, args, callback) {
assert(Array.isArray(args));
assert.strictEqual(typeof callback, 'function');
taskdb.add({ type: type, percent: 0, message: 'Queued', args: args }, function (error, taskId) {
taskdb.add({ type: type, percent: 1, message: 'Queued', args: args }, function (error, taskId) {
if (error) return callback(error);
callback(null, taskId);
});
}
function startTask(id, options, callback) {
assert.strictEqual(typeof id, 'string');
function startTask(taskId, options, callback) {
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
const logFile = options.logFile || `${paths.TASKS_LOG_DIR}/${id}.log`;
debug(`startTask - starting task ${id}. logs at ${logFile}`);
const logFile = options.logFile || `${paths.TASKS_LOG_DIR}/${taskId}.log`;
let fd = safe.fs.openSync(logFile, 'a'); // will autoclose. append is for apptask logs
if (!fd) {
debug(`startTask: unable to get log filedescriptor ${safe.error.message}`);
return callback(new BoxError(BoxError.FS_ERROR, safe.error));
}
debug(`startTask - starting task ${taskId}. logs at ${logFile}`);
let killTimerId = null, timedOut = false;
gTasks[id] = shell.sudo('startTask', [ START_TASK_CMD, id, logFile, options.nice || 0, options.memoryLimit || 400 ], { preserveEnv: true }, function (error) {
if (!gTasks[id]) return; // ignore task exit since we are shutting down. see stopAllTasks
const code = error ? error.code : 0;
const signal = error ? error.signal : 0;
debug(`startTask: ${id} completed with code ${code} and signal ${signal}`);
gTasks[taskId] = child_process.fork(`${__dirname}/taskworker.js`, [ taskId ], { stdio: [ 'pipe', fd, fd, 'ipc' ]}); // fork requires ipc
gTasks[taskId].once('exit', function (code, signal) {
debug(`startTask: ${taskId} completed with code ${code} and signal ${signal}`);
if (options.timeout) clearTimeout(killTimerId);
get(id, function (getError, task) {
get(taskId, function (error, task) {
let taskError;
if (!getError && task.percent !== 100) { // taskworker crashed or was killed by us
if (!error && task.percent !== 100) { // task crashed or was killed by us
taskError = {
message: code === 0 ? `Task ${id} ${timedOut ? 'timed out' : 'stopped'}` : `Task ${id} crashed with code ${code} and signal ${signal}`,
message: code === 0 ? `Task ${taskId} ${timedOut ? 'timed out' : 'stopped'}` : `Task ${taskId} crashed with code ${code} and signal ${signal}`,
code: code === 0 ? (timedOut ? exports.ETIMEOUT : exports.ESTOPPED) : exports.ECRASHED
};
// note that despite the update() here, we should handle the case where the box code was restarted and never got taskworker exit
setCompleted(id, { error: taskError }, NOOP_CALLBACK);
} else if (!getError && task.error) {
setCompleted(taskId, { error: taskError }, NOOP_CALLBACK);
} else if (!error && task.error) {
taskError = task.error;
} else if (!task) { // db got cleared in tests
taskError = new BoxError(BoxError.NOT_FOUND, `No such task ${id}`);
taskError = new BoxError(BoxError.NOT_FOUND, `No such task ${taskId}`);
}
delete gTasks[id];
delete gTasks[taskId];
callback(taskError, task ? task.result : null);
debug(`startTask: ${id} done`);
debug(`startTask: ${taskId} done`);
});
});
if (options.timeout) {
killTimerId = setTimeout(function () {
debug(`startTask: task ${id} took too long. killing`);
debug(`startTask: task ${taskId} took too long. killing`);
timedOut = true;
stopTask(id, NOOP_CALLBACK);
stopTask(taskId, NOOP_CALLBACK);
}, options.timeout);
}
}
@@ -198,7 +194,7 @@ function stopTask(id, callback) {
debug(`stopTask: stopping task ${id}`);
shell.sudo('stopTask', [ STOP_TASK_CMD, id, ], {}, NOOP_CALLBACK);
gTasks[id].kill('SIGTERM'); // this will end up calling the 'exit' signal handler
callback(null);
}
@@ -206,10 +202,9 @@ function stopTask(id, callback) {
function stopAllTasks(callback) {
assert.strictEqual(typeof callback, 'function');
debug('stopTask: stopping all tasks');
gTasks = {}; // this signals startTask() to not set completion status as "crashed"
shell.sudo('stopTask', [ STOP_TASK_CMD, 'all' ], {}, callback);
async.eachSeries(Object.keys(gTasks), function (id, iteratorDone) {
stopTask(id, () => iteratorDone()); // ignore any error
}, callback);
}
function listByTypePaged(type, page, perPage, callback) {
@@ -277,6 +272,6 @@ function getLogs(taskId, options, callback) {
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields(task) {
var result = _.pick(task, 'id', 'type', 'percent', 'message', 'error', 'active', 'pending', 'creationTime', 'result', 'ts', 'success');
var result = _.pick(task, 'id', 'type', 'percent', 'message', 'error', 'active', 'creationTime', 'result', 'ts', 'success');
return result;
}
+30 -62
View File
@@ -1,103 +1,71 @@
#!/usr/bin/env node
'use strict';
require('supererror')({ splatchError: true });
var apptask = require('./apptask.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
cloudron = require('./cloudron.js'),
database = require('./database.js'),
debug = require('debug')('box:taskworker'),
domains = require('./domains.js'),
externalLdap = require('./externalldap.js'),
fs = require('fs'),
mail = require('./mail.js'),
reverseProxy = require('./reverseproxy.js'),
settings = require('./settings.js'),
tasks = require('./tasks.js'),
updater = require('./updater.js'),
util = require('util');
updater = require('./updater.js');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const TASKS = { // indexed by task type
app: apptask.run,
backup: backups.backupBoxAndApps,
update: updater.update,
renewcerts: reverseProxy.renewCerts,
setupDnsAndCert: cloudron.setupDnsAndCert,
prepareDashboardDomain: domains.prepareDashboardDomain,
cleanBackups: backups.cleanup,
syncExternalLdap: externalLdap.sync,
changeMailLocation: mail.changeLocation,
_identity: (arg, progressCallback, callback) => callback(null, arg),
_error: (arg, progressCallback, callback) => callback(new Error(`Failed for arg: ${arg}`)),
_crash: (arg) => { throw new Error(`Crashing for arg: ${arg}`); }, // the test looks for this debug string in the log file
_crash: (arg) => { throw new Error(`Crashing for arg: ${arg}`); },
_sleep: (arg) => setTimeout(process.exit, arg)
};
if (process.argv.length !== 4) {
console.error('Pass the taskid and logfile as argument');
process.exit(1);
}
process.on('SIGTERM', function () {
process.exit(0);
});
assert.strictEqual(process.argv.length, 3, 'Pass the taskid as argument');
const taskId = process.argv[2];
const logFile = process.argv[3];
function setupLogging(callback) {
fs.open(logFile, 'a', function (error, fd) {
if (error) return callback(error);
require('debug').log = function (...args) {
fs.appendFileSync(fd, util.format(...args) + '\n');
};
callback();
});
function initialize(callback) {
async.series([
database.initialize,
settings.initCache
], callback);
}
// Main process starts here
const startTime = new Date();
debug(`Starting task ${taskId}`);
async.series([
setupLogging,
database.initialize,
settings.initCache
], function (error) {
initialize(function (error) {
if (error) return process.exit(50);
const debug = require('debug')('box:taskworker'); // require this here so that logging handler is already setup
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
process.on('SIGTERM', () => process.exit(0)); // sent as timeout notification
debug(`Starting task ${taskId}. Logs are at ${logFile}`);
tasks.get(taskId, function (error, task) {
if (error) return process.exit(50);
tasks.update(taskId, { percent: 2, error: null }, function (error) {
if (error) {
console.error(error);
return process.exit(50);
}
const progressCallback = (progress, cb) => tasks.update(taskId, progress, cb || NOOP_CALLBACK);
const resultCallback = (error, result) => {
// Error object has properties with enumerable: false (https://mattcbaker.com/posts/stringify-javascript-error/)
const progress = {
result: result || null,
error: error ? JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error))) : null
};
debug(`Task took ${(new Date() - startTime)/1000} seconds`);
tasks.setCompleted(taskId, progress, () => process.exit(error ? 50 : 0));
const progressCallback = (progress, cb) => tasks.update(taskId, progress, cb || NOOP_CALLBACK);
const resultCallback = (error, result) => {
// Error object has properties with enumerable: false (https://mattcbaker.com/posts/stringify-javascript-error/)
const progress = {
result: result || null,
error: error ? JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error))) : null
};
try {
TASKS[task.type].apply(null, task.args.concat(progressCallback).concat(resultCallback));
} catch (error) {
debug('Uncaught exception in task', error);
process.exit(1); // do not call setCompleted() intentionally. the task code must be resilient enough to handle it
}
});
tasks.setCompleted(taskId, progress, () => process.exit(error ? 50 : 0));
};
TASKS[task.type].apply(null, task.args.concat(progressCallback).concat(resultCallback));
});
});

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