Compare commits

..

1 Commits

Author SHA1 Message Date
Girish Ramakrishnan cc6ddf50b1 5.0.5 changes 2020-03-25 07:57:39 -07:00
92 changed files with 2192 additions and 2909 deletions
-102
View File
@@ -1850,105 +1850,3 @@
* Increase token expiry
* Fix bug in tag UI where tag removal did not work
[5.0.6]
* Make mail eventlog only visible to owners
* Make app password work with sftp
[5.1.0]
* Add turn addon
* Fix disk usage display
* Drop support for TLSv1 and TLSv1.1
* Make cert validation work for ECC certs
* Add type filter to mail eventlog
* mail: Fix listing of mailboxes and aliases in the UI
* branding: fix login page title
* Only a Cloudron owner can install/update/exec apps with the docker addon
* security: reset tokens are only valid for a day
* mail: fix eventlog db perms
* Fix various bugs in the disk graphs
[5.1.1]
* Add turn addon
* Fix disk usage display
* Drop support for TLSv1 and TLSv1.1
* Make cert validation work for ECC certs
* Add type filter to mail eventlog
* mail: Fix listing of mailboxes and aliases in the UI
* branding: fix login page title
* Only a Cloudron owner can install/update/exec apps with the docker addon
* security: reset tokens are only valid for a day
* mail: fix eventlog db perms
* Fix various bugs in the disk graphs
* Fix collectd installation
* graphs: sort disk contents by usage
* backups: show apps that are not automatically backed up in backup view
[5.1.2]
* Add turn addon
* Fix disk usage display
* Drop support for TLSv1 and TLSv1.1
* Make cert validation work for ECC certs
* Add type filter to mail eventlog
* mail: Fix listing of mailboxes and aliases in the UI
* branding: fix login page title
* Only a Cloudron owner can install/update/exec apps with the docker addon
* security: reset tokens are only valid for a day
* mail: fix eventlog db perms
* Fix various bugs in the disk graphs
* Fix collectd installation
* graphs: sort disk contents by usage
* backups: show apps that are not automatically backed up in backup view
* turn: deny local address peers https://www.rtcsec.com/2020/04/01-slack-webrtc-turn-compromise/
[5.1.3]
* Fix crash with misconfigured reverse proxy
* Fix issue where invitation links are not working anymore
[5.1.4]
* Add support for custom .well-known documents to be served
* Add ECDHE-RSA-AES128-SHA256 to cipher list
* Fix GPG signature verification
[5.1.5]
* Check for .well-known routes upstream as fallback. This broke nextcloud's caldav/carddav
[5.2.0]
* acme: request ECC certs
* less-strict DKIM check to allow users to set a stronger DKIM key
* Add members only flag to mailing list
* oauth: add backward compat layer for backup and uninstall
* fix bug in disk usage sorting
* mail: aliases can be across domains
* mail: allow an external MX to be set
* Add UI to download backup config as JSON (and import it)
* Ensure stopped apps are getting backed up
* Add OVH Object Storage backend
* Add per-app redis status and configuration to Services
* spam: large emails were not scanned
* mail relay: fix delivery event log
* manual update check always gets the latest updates
* graphs: fix issue where large number of apps would crash the box code (query param limit exceeded)
* backups: fix various security issues in encypted backups (thanks @mehdi)
* graphs: add app graphs
* older encrypted backups cannot be used in this version
* Add backup listing UI
* stopping an app will stop dependent services
* Add new wasabi s3 storage region us-east-2
* mail: Fix bug where SRS translation was done on the main domain instead of mailing list domain
[5.2.1]
* Fix app disk graphs
* restart apps on addon container change
[5.2.2]
* regression: import UI
* Mbps -> MBps
* Remove verbose logs
* Set dmode in tar extract
* mail: fix crash in audit logs
* import: fix crash because encryption is unset
* create redis with the correct label
[5.2.3]
* Do not restart stopped apps
+12 -2
View File
@@ -48,8 +48,18 @@ the dashboard, database addons, graph container, base image etc. Cloudron also r
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.
## Support
## Documentation
* [Documentation](https://cloudron.io/documentation/)
* [Forum](https://forum.cloudron.io/)
## Related repos
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
the containers in the Cloudron.
## Community
* [Chat](https://chat.cloudron.io)
* [Forum](https://forum.cloudron.io/)
* [Support](mailto:support@cloudron.io)
+2 -12
View File
@@ -44,6 +44,7 @@ apt-get -y install \
linux-generic \
logrotate \
mysql-server-5.7 \
nginx-full \
openssh-server \
pwgen \
resolvconf \
@@ -53,17 +54,6 @@ apt-get -y install \
unbound \
xfsprogs
if [[ "${ubuntu_version}" == "16.04" ]]; then
echo "==> installing nginx for xenial for TLSv3 support"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/nginx.deb
rm /tmp/nginx.deb
else
apt install -y nginx-full
fi
# on some providers like scaleway the sudo file is changed and we want to keep the old one
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
@@ -121,7 +111,7 @@ for image in ${images}; do
done
echo "==> Install collectd"
if ! apt-get install -y libcurl3-gnutls collectd collectd-utils; then
if ! apt-get install -y collectd collectd-utils; then
# FQDNLookup is true in default debian config. The box code has a custom collectd.conf that fixes this
echo "Failed to install collectd. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN resetTokenCreationTime', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,28 +0,0 @@
'use strict';
let async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY mailboxDomain VARCHAR(128)', [], function (error) { // make it nullable
if (error) console.error(error);
// clear mailboxName/Domain for apps that do not use mail addons
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, iteratorDone) {
var manifest = JSON.parse(app.manifestJson);
if (manifest.addons['sendmail'] || manifest.addons['recvmail']) return iteratorDone();
db.runSql('UPDATE apps SET mailboxName=?, mailboxDomain=? WHERE id=?', [ null, null, app.id ], iteratorDone);
}, callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY manifestJson VARCHAR(128) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,17 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mailboxes ADD COLUMN membersOnly BOOLEAN DEFAULT 0', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN membersOnly', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,28 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN aliasDomain VARCHAR(128)'),
function setAliasDomain(done) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (!mailbox.aliasTarget) return iteratorDone();
db.runSql('UPDATE mailboxes SET aliasDomain=? WHERE name=? AND domain=?', [ mailbox.domain, mailbox.name, mailbox.domain ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_aliasDomain_constraint FOREIGN KEY(aliasDomain) REFERENCES mail(domain)'),
db.runSql.bind(db, 'ALTER TABLE mailboxes CHANGE aliasTarget aliasName VARCHAR(128)')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_aliasDomain_constraint'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP COLUMN aliasDomain'),
db.runSql.bind(db, 'ALTER TABLE mailboxes CHANGE aliasName aliasTarget VARCHAR(128)')
], callback);
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN servicesConfigJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN servicesConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN bindsJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN bindsJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,35 +0,0 @@
'use strict';
const backups = require('../src/backups.js'),
fs = require('fs');
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.key) {
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.key);
backups.cleanupCacheFilesSync();
fs.writeFileSync('/home/yellowtent/platformdata/BACKUP_PASSWORD',
'This file contains your Cloudron backup password.\nBefore Cloudron v5.2, this was saved in the database.' +
'From Cloudron 5.2, this password is not required anymore. We generate strong keys based off this password and use those keys to encrypt the backups.\n' +
'This means that the password is only required at decryption/restore time.\n\n' +
'This file can be safely removed and only exists for the off-chance that you do not remember your backup password.\n\n' +
`Password: ${backupConfig.key}\n`,
'utf8');
} else {
backupConfig.encryption = null;
}
delete backupConfig.key;
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups CHANGE version packageVersion VARCHAR(128) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups CHANGE packageVersion version VARCHAR(128) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,24 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups ADD COLUMN encryptionVersion INTEGER', function (error) {
if (error) return callback(error);
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.encryption) return callback(null);
// mark old encrypted backups as v1
db.runSql('UPDATE backups SET encryptionVersion=1', callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups DROP COLUMN encryptionVersion', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,18 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
backupConfig.retentionPolicy = { keepWithinSecs: backupConfig.retentionSecs };
delete backupConfig.retentionSecs;
// mark old encrypted backups as v1
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
+4 -12
View File
@@ -28,9 +28,6 @@ CREATE TABLE IF NOT EXISTS users(
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
source VARCHAR(128) DEFAULT "",
role VARCHAR(32),
resetToken VARCHAR(128) DEFAULT "",
resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
active BOOLEAN DEFAULT 1,
PRIMARY KEY(id));
@@ -79,14 +76,13 @@ CREATE TABLE IF NOT EXISTS apps(
reverseProxyConfigJson TEXT, // { robotsTxt, csp }
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
enableAutomaticUpdate BOOLEAN DEFAULT 1,
mailboxName VARCHAR(128), // mailbox of this app
mailboxDomain VARCHAR(128), // mailbox domain of this apps
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
mailboxDomain VARCHAR(128) NOT NULL, // mailbox domain of this apps
label VARCHAR(128), // display name
tagsJson VARCHAR(2048), // array of tags
dataDir VARCHAR(256) UNIQUE,
taskId INTEGER, // current task
errorJson TEXT,
bindsJson TEXT, // bind mounts
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(taskId) REFERENCES tasks(id),
@@ -121,8 +117,7 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
packageVersion VARCHAR(128) NOT NULL, /* app version or box version */
encryptionVersion INTEGER, /* when null, unencrypted backup */
version VARCHAR(128) NOT NULL, /* app version or box version */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
dependsOn TEXT, /* comma separate list of objects this backup depends on */
state VARCHAR(16) NOT NULL,
@@ -179,15 +174,12 @@ CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* user id */
aliasName VARCHAR(128), /* the target name type is an alias */
aliasDomain VARCHAR(128), /* the target domain */
aliasTarget VARCHAR(128), /* the target name type is an alias */
membersJson TEXT, /* members of a group. fully qualified */
membersOnly BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain),
FOREIGN KEY(aliasDomain) REFERENCES mail(domain),
UNIQUE (name, domain));
CREATE TABLE IF NOT EXISTS subdomains(
+57 -57
View File
@@ -338,7 +338,7 @@
},
"amdefine": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true
},
@@ -411,7 +411,7 @@
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"assertion-error": {
@@ -483,7 +483,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"
@@ -621,7 +621,7 @@
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"buffer-fill": {
@@ -636,7 +636,7 @@
},
"bunyan": {
"version": "1.8.12",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
"resolved": false,
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
"requires": {
"dtrace-provider": "~0.8",
@@ -736,22 +736,22 @@
}
},
"cloudron-manifestformat": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.1.1.tgz",
"integrity": "sha512-1mArahTp9qkYRQsUJfpT/x6est1qW+gKPF+HoFU0hPuOVuBdMkfu6UUmwZDYmQF4FrbQkir46GyQAJADaXBg6g==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-4.0.0.tgz",
"integrity": "sha512-St/Quu8ofQOf0rUAMaIsOL0u0dZ46irweU8rYVMvAXU0CGwSD9KDaeLW5NjGRg3FVjNzladUDVUE/BGD4rwEvA==",
"requires": {
"cron": "^1.8.2",
"cron": "^1.7.2",
"java-packagename-regex": "^1.0.0",
"safetydance": "1.0.0",
"semver": "^7.1.3",
"safetydance": "0.7.1",
"semver": "^6.3.0",
"tv4": "^1.3.0",
"validator": "^12.2.0"
"validator": "^12.0.0"
},
"dependencies": {
"semver": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz",
"integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA=="
"safetydance": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.7.1.tgz",
"integrity": "sha1-FOtNQqHKr8UUVVK2zmnJuIJN0qo="
},
"validator": {
"version": "12.2.0",
@@ -762,7 +762,7 @@
},
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true
},
@@ -807,7 +807,7 @@
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
@@ -992,7 +992,7 @@
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cron": {
@@ -1034,7 +1034,7 @@
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"resolved": false,
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"requires": {
"assert-plus": "^1.0.0"
@@ -1109,7 +1109,7 @@
},
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"resolved": false,
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"deep-eql": {
@@ -1376,7 +1376,7 @@
},
"ent": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
"resolved": false,
"integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0="
},
"error-ex": {
@@ -1480,7 +1480,7 @@
},
"expect.js": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz",
"resolved": false,
"integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=",
"dev": true
},
@@ -2267,7 +2267,7 @@
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"resolved": false,
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
@@ -2303,7 +2303,7 @@
},
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"resolved": false,
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
@@ -2385,12 +2385,12 @@
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"isstream": {
@@ -2526,7 +2526,7 @@
},
"ldap-filter": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
"resolved": false,
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
"requires": {
"assert-plus": "0.1.5"
@@ -2534,7 +2534,7 @@
"dependencies": {
"assert-plus": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"resolved": false,
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
}
}
@@ -2733,19 +2733,19 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"requires": {
"minimist": "0.0.8"
@@ -2876,9 +2876,9 @@
}
},
"moment": {
"version": "2.25.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.25.3.tgz",
"integrity": "sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg=="
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"moment-timezone": {
"version": "0.5.27",
@@ -2943,7 +2943,7 @@
},
"mv": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"resolved": false,
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"optional": true,
"requires": {
@@ -2954,7 +2954,7 @@
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"resolved": false,
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"optional": true,
"requires": {
@@ -2967,13 +2967,13 @@
},
"ncp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
"optional": true
},
"rimraf": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"resolved": false,
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"optional": true,
"requires": {
@@ -3261,7 +3261,7 @@
},
"nopt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
"resolved": false,
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
"dev": true,
"requires": {
@@ -3310,7 +3310,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true
},
@@ -3470,7 +3470,7 @@
},
"parse-json": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
"resolved": false,
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
"dev": true,
"requires": {
@@ -3494,7 +3494,7 @@
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-key": {
@@ -3584,7 +3584,7 @@
},
"precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"resolved": false,
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"pretty-bytes": {
@@ -3652,7 +3652,7 @@
},
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
"dev": true
},
@@ -3920,7 +3920,7 @@
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"resolved": false,
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-main-filename": {
@@ -4194,7 +4194,7 @@
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"setprototypeof": {
@@ -4254,7 +4254,7 @@
},
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"smtp-connection": {
@@ -4348,7 +4348,7 @@
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"resolved": false,
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sqlstring": {
@@ -4515,7 +4515,7 @@
},
"stubs": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
"resolved": false,
"integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls="
},
"superagent": {
@@ -4812,7 +4812,7 @@
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"resolved": false,
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"uid-safe": {
@@ -4884,7 +4884,7 @@
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utile": {
@@ -4939,7 +4939,7 @@
},
"vasync": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
"resolved": false,
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
"requires": {
"verror": "1.6.0"
@@ -4947,7 +4947,7 @@
"dependencies": {
"verror": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
"resolved": false,
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
"requires": {
"extsprintf": "1.2.0"
@@ -4957,7 +4957,7 @@
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"resolved": false,
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"requires": {
"assert-plus": "^1.0.0",
@@ -4980,7 +4980,7 @@
},
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wide-align": {
@@ -5067,7 +5067,7 @@
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write-file-atomic": {
+1 -2
View File
@@ -20,7 +20,7 @@
"async": "^2.6.3",
"aws-sdk": "^2.610.0",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^5.1.1",
"cloudron-manifestformat": "^4.0.0",
"connect": "^3.7.0",
"connect-lastmile": "^1.2.2",
"connect-timeout": "^1.9.0",
@@ -39,7 +39,6 @@
"lodash": "^4.17.15",
"lodash.chunk": "^4.2.0",
"mime": "^2.4.4",
"moment": "^2.25.3",
"moment-timezone": "^0.5.27",
"morgan": "^1.9.1",
"multiparty": "^4.2.1",
+2 -5
View File
@@ -13,7 +13,7 @@ HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
Options:
--owner-login Login as owner
--admin-login Login as administrator
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
"
@@ -26,7 +26,7 @@ fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,enable-ssh,admin-login" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -34,9 +34,6 @@ while true; do
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
--enable-ssh) enableSSH="true"; shift;;
--admin-login)
# fall through
;&
--owner-login)
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' LIMIT 1" 2>/dev/null)
admin_password=$(pwgen -1s 12)
ghost_file=/home/yellowtent/platformdata/cloudron_ghost.json
+10 -20
View File
@@ -11,8 +11,9 @@ if [[ ${EUID} -ne 0 ]]; then
exit 1
fi
readonly user=yellowtent
readonly box_src_dir=/home/${user}/box
readonly USER=yellowtent
readonly BOX_SRC_DIR=/home/${USER}/box
readonly BASE_DATA_DIR=/home/${USER}
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -23,8 +24,6 @@ readonly ubuntu_codename=$(lsb_release -cs)
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
echo "==> installer: Updating from $(cat $box_src_dir/VERSION) to $(cat $box_src_tmp_dir/VERSION) <=="
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
@@ -57,15 +56,6 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
fi
readonly nginx_version=$(nginx -v)
if [[ "${nginx_version}" != *"1.14."* && "${ubuntu_version}" == "16.04" ]]; then
echo "==> installer: installing nginx for xenial for TLSv3 support"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
rm /tmp/nginx.deb
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v10.18.1" ]]; then
mkdir -p /usr/local/node-10.18.1
@@ -119,22 +109,22 @@ while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLO
sleep 5
done
if ! id "${user}" 2>/dev/null; then
useradd "${user}" -m
if ! id "${USER}" 2>/dev/null; then
useradd "${USER}" -m
fi
if [[ "${is_update}" == "yes" ]]; then
echo "==> installer: stop cloudron.target service for update"
${box_src_dir}/setup/stop.sh
${BOX_SRC_DIR}/setup/stop.sh
fi
# ensure we are not inside the source directory, which we will remove now
cd /root
echo "==> installer: switching the box code"
rm -rf "${box_src_dir}"
mv "${box_src_tmp_dir}" "${box_src_dir}"
chown -R "${user}:${user}" "${box_src_dir}"
rm -rf "${BOX_SRC_DIR}"
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}"
echo "==> installer: calling box setup script"
"${box_src_dir}/setup/start.sh"
"${BOX_SRC_DIR}/setup/start.sh"
+1 -12
View File
@@ -20,11 +20,6 @@ readonly ubuntu_version=$(lsb_release -rs)
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
# this needs to match the cloudron/base:2.0.0 gid
if ! getent group media; then
addgroup --gid 500 --system media
fi
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
systemctl enable apparmor
@@ -61,7 +56,6 @@ mkdir -p "${BOX_DATA_DIR}/profileicons"
mkdir -p "${BOX_DATA_DIR}/certs"
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
mkdir -p "${BOX_DATA_DIR}/mail/dkim"
mkdir -p "${BOX_DATA_DIR}/well-known" # .well-known documents
# ensure backups folder exists and is writeable
mkdir -p /var/backups
@@ -176,11 +170,9 @@ readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
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
# paths.js uses this env var and some of the migrate code requires box code
echo "==> Migrating data"
cd "${BOX_SRC_DIR}"
if ! HOME=${HOME_DIR} BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up; then
if ! BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up; then
echo "DB migration failed"
exit 1
fi
@@ -199,9 +191,6 @@ fi
echo "==> Cleaning up stale redis directories"
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} +
echo "==> Cleaning up old logs"
rm -f /home/yellowtent/platformdata/logs/*/*.log.* || true
echo "==> Changing ownership"
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
chown -R "${USER}" /etc/cloudron
-5
View File
@@ -12,11 +12,6 @@ iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
# ssh is allowed alternately on port 202
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT
# turn and stun service
iptables -t filter -A CLOUDRON -p tcp -m multiport --dports 3478,5349 -j ACCEPT
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 3478,5349 -j ACCEPT
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 50000:51000 -j ACCEPT
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
iptables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT
+1 -4
View File
@@ -3,12 +3,10 @@ import collectd,os,subprocess,sys,re,time
# https://www.programcreek.com/python/example/106897/collectd.register_read
PATHS = [] # { name, dir, exclude }
# there is a pattern in carbon/storage-schemas.conf which stores values every 12h for a year
INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
def du(pathinfo):
# -B1 makes du print block sizes and not apparent sizes (to match df which also uses block sizes)
cmd = 'timeout 1800 du -DsB1 "{}"'.format(pathinfo['dir'])
cmd = 'timeout 1800 du -Dsb "{}"'.format(pathinfo['dir'])
if pathinfo['exclude'] != '':
cmd += ' --exclude "{}"'.format(pathinfo['exclude'])
@@ -28,7 +26,6 @@ def parseSize(size):
def dockerSize():
# use --format '{{json .}}' to dump the string. '{{if eq .Type "Images"}}{{.Size}}{{end}}' still creates newlines
# https://godoc.org/github.com/docker/go-units#HumanSize is used. so it's 1000 (KB) and not 1024 (KiB)
cmd = 'timeout 1800 docker system df --format "{{.Size}}" | head -n1'
try:
size = subprocess.check_output(cmd, shell=True).strip().decode('utf-8')
-2
View File
@@ -10,7 +10,6 @@
/home/yellowtent/platformdata/logs/redis-*/*.log
/home/yellowtent/platformdata/logs/crash/*.log
/home/yellowtent/platformdata/logs/collectd/*.log
/home/yellowtent/platformdata/logs/turn/*.log
/home/yellowtent/platformdata/logs/updater/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
@@ -18,7 +17,6 @@
missingok
# we never compress so we can simply tail the files
nocompress
# this truncates the original log file and not the rotated one
copytruncate
}
+1 -1
View File
@@ -13,7 +13,7 @@ Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
; 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'
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well
KillMode=control-group
+143 -361
View File
@@ -7,9 +7,6 @@ exports = module.exports = {
getServiceLogs: getServiceLogs,
restartService: restartService,
startAppServices,
stopAppServices,
startServices: startServices,
updateServiceConfig: updateServiceConfig,
@@ -23,7 +20,11 @@ exports = module.exports = {
getMountsSync: getMountsSync,
getContainerNamesSync: getContainerNamesSync,
getContainerDetails: getContainerDetails,
getServiceDetails: getServiceDetails,
// exported for testing
_setupOauth: setupOauth,
_teardownOauth: teardownOauth,
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
SERVICE_STATUS_ACTIVE: 'active',
@@ -65,14 +66,7 @@ const RMADDONDIR_CMD = path.join(__dirname, 'scripts/rmaddondir.sh');
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
// teardown is destructive. app data stored with the addon is lost
var ADDONS = {
turn: {
setup: setupTurn,
teardown: teardownTurn,
backup: NOOP,
restore: NOOP,
clear: NOOP
},
var KNOWN_ADDONS = {
email: {
setup: setupEmail,
teardown: teardownEmail,
@@ -108,6 +102,13 @@ var ADDONS = {
restore: restoreMySql,
clear: clearMySql,
},
oauth: {
setup: setupOauth,
teardown: teardownOauth,
backup: NOOP,
restore: setupOauth,
clear: NOOP,
},
postgresql: {
setup: setupPostgreSql,
teardown: teardownPostgreSql,
@@ -149,23 +150,10 @@ var ADDONS = {
backup: NOOP,
restore: NOOP,
clear: NOOP,
},
oauth: { // kept for backward compatibility. keep teardown for uninstall to work
setup: NOOP,
teardown: teardownOauth,
backup: NOOP,
restore: NOOP,
clear: NOOP,
}
};
// services are actual containers that are running. addons are the concepts requested by app
const SERVICES = {
turn: {
status: statusTurn,
restart: restartContainer.bind(null, 'turn'),
defaultMemoryLimit: 256 * 1024 * 1024
},
const KNOWN_SERVICES = {
mail: {
status: containerStatus.bind(null, 'mail', 'CLOUDRON_MAIL_TOKEN'),
restart: mail.restartMail,
@@ -213,16 +201,6 @@ const SERVICES = {
}
};
const APP_SERVICES = {
redis: {
status: (instance, done) => containerStatus(`redis-${instance}`, 'CLOUDRON_REDIS_TOKEN', done),
start: (instance, done) => docker.startContainer(`redis-${instance}`, done),
stop: (instance, done) => docker.stopContainer(`redis-${instance}`, done),
restart: (instance, done) => restartContainer(`redis-${instance}`, done),
defaultMemoryLimit: 150 * 1024 * 1024
}
};
function debugApp(app /*, args */) {
assert(typeof app === 'object');
@@ -259,7 +237,6 @@ function rebuildService(serviceName, callback) {
// this attempts to recreate the service docker container if they don't exist but platform infra version is unchanged
// passing an infra version of 'none' will not attempt to purge existing data, not sure if this is good or bad
if (serviceName === 'turn') return startTurn({ version: 'none' }, callback);
if (serviceName === 'mongodb') return startMongodb({ version: 'none' }, callback);
if (serviceName === 'postgresql') return startPostgresql({ version: 'none' }, callback);
if (serviceName === 'mysql') return startMysql({ version: 'none' }, callback);
@@ -270,22 +247,25 @@ function rebuildService(serviceName, callback) {
callback();
}
function restartContainer(name, callback) {
assert.strictEqual(typeof name, 'string');
function restartContainer(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
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) console.error(`Unable to rebuild service ${name}`, error); });
}
docker.stopContainer(serviceName, function (error) {
if (error) return callback(error);
callback(error);
docker.startContainer(serviceName, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) {
callback(null); // callback early since rebuilding takes long
return rebuildService(serviceName, function (error) { if (error) console.error(`Unable to rebuild service ${serviceName}`, error); });
}
callback(error);
});
});
}
function getContainerDetails(containerName, tokenEnvName, callback) {
function getServiceDetails(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -308,20 +288,20 @@ function getContainerDetails(containerName, tokenEnvName, callback) {
});
}
function containerStatus(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string');
function containerStatus(addonName, addonTokenName, callback) {
assert.strictEqual(typeof addonName, 'string');
assert.strictEqual(typeof addonTokenName, 'string');
assert.strictEqual(typeof callback, 'function');
getContainerDetails(containerName, tokenEnvName, function (error, addonDetails) {
if (error && (error.reason === BoxError.NOT_FOUND || error.reason === BoxError.INACTIVE)) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
getServiceDetails(addonName, addonTokenName, function (error, addonDetails) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(error);
request.get(`https://${addonDetails.ip}:3000/healthcheck?access_token=${addonDetails.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${containerName}: ${error.message}` });
if (response.statusCode !== 200 || !response.body.status) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}` });
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${addonName}: ${error.message}` });
if (response.statusCode !== 200 || !response.body.status) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${addonName}. Status code: ${response.statusCode} message: ${response.body.message}` });
docker.memoryUsage(containerName, function (error, result) {
docker.memoryUsage(addonName, function (error, result) {
if (error) return callback(error);
var tmp = {
@@ -339,59 +319,19 @@ function containerStatus(containerName, tokenEnvName, callback) {
function getServices(callback) {
assert.strictEqual(typeof callback, 'function');
let services = Object.keys(SERVICES);
let services = Object.keys(KNOWN_SERVICES);
appdb.getAll(function (error, apps) {
if (error) return callback(error);
for (let app of apps) {
if (app.manifest.addons && app.manifest.addons['redis']) services.push(`redis:${app.id}`);
}
callback(null, services);
});
callback(null, services);
}
function getServicesConfig(id, callback) {
assert.strictEqual(typeof id, 'string');
function getService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
const [name, instance ] = id.split(':');
if (!instance) {
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(error);
callback(null, SERVICES[name], platformConfig);
});
return;
}
appdb.get(instance, function (error, app) {
if (error) return callback(error);
callback(null, APP_SERVICES[name], app.servicesConfig);
});
}
function getService(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
const [name, instance ] = id.split(':');
let containerStatusFunc;
if (instance) {
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
containerStatusFunc = APP_SERVICES[name].status.bind(null, instance);
} else if (SERVICES[name]) {
containerStatusFunc = SERVICES[name].status;
} else {
return callback(new BoxError(BoxError.NOT_FOUND));
}
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
var tmp = {
name: name,
name: serviceName,
status: null,
memoryUsed: 0,
memoryPercent: 0,
@@ -403,76 +343,60 @@ function getService(id, callback) {
}
};
containerStatusFunc(function (error, result) {
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(error);
tmp.status = result.status;
tmp.memoryUsed = result.memoryUsed;
tmp.memoryPercent = result.memoryPercent;
tmp.error = result.error || null;
if (platformConfig[serviceName] && platformConfig[serviceName].memory && platformConfig[serviceName].memorySwap) {
tmp.config.memory = platformConfig[serviceName].memory;
tmp.config.memorySwap = platformConfig[serviceName].memorySwap;
} else if (KNOWN_SERVICES[serviceName].defaultMemoryLimit) {
tmp.config.memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
tmp.config.memorySwap = tmp.config.memory * 2;
}
getServicesConfig(id, function (error, service, servicesConfig) {
KNOWN_SERVICES[serviceName].status(function (error, result) {
if (error) return callback(error);
const serviceConfig = servicesConfig[name];
if (serviceConfig && serviceConfig.memory && serviceConfig.memorySwap) {
tmp.config.memory = serviceConfig.memory;
tmp.config.memorySwap = serviceConfig.memorySwap;
} else if (service.defaultMemoryLimit) {
tmp.config.memory = service.defaultMemoryLimit;
tmp.config.memorySwap = tmp.config.memory * 2;
}
tmp.status = result.status;
tmp.memoryUsed = result.memoryUsed;
tmp.memoryPercent = result.memoryPercent;
tmp.error = result.error || null;
callback(null, tmp);
});
});
}
function configureService(id, data, callback) {
assert.strictEqual(typeof id, 'string');
function configureService(serviceName, data, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
const [name, instance ] = id.split(':');
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
if (instance) {
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
} else if (!SERVICES[name]) {
return callback(new BoxError(BoxError.NOT_FOUND));
}
getServicesConfig(id, function (error, service, servicesConfig) {
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(error);
if (!servicesConfig[name]) servicesConfig[name] = {};
if (!platformConfig[serviceName]) platformConfig[serviceName] = {};
// if not specified we clear the entry and use defaults
if (!data.memory || !data.memorySwap) {
delete servicesConfig[name];
delete platformConfig[serviceName];
} else {
servicesConfig[name].memory = data.memory;
servicesConfig[name].memorySwap = data.memorySwap;
platformConfig[serviceName].memory = data.memory;
platformConfig[serviceName].memorySwap = data.memorySwap;
}
if (instance) {
appdb.update(instance, { servicesConfig }, function (error) {
if (error) return callback(error);
settings.setPlatformConfig(platformConfig, function (error) {
if (error) return callback(error);
updateAppServiceConfig(name, instance, servicesConfig, callback);
});
} else {
settings.setPlatformConfig(servicesConfig, function (error) {
if (error) return callback(error);
callback(null);
});
}
callback(null);
});
});
}
function getServiceLogs(id, options, callback) {
assert.strictEqual(typeof id, 'string');
function getServiceLogs(serviceName, options, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
@@ -480,15 +404,9 @@ function getServiceLogs(id, options, callback) {
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
const [name, instance ] = id.split(':');
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
if (instance) {
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
} else if (!SERVICES[name]) {
return callback(new BoxError(BoxError.NOT_FOUND));
}
debug(`Getting logs for ${name}`);
debug(`Getting logs for ${serviceName}`);
var lines = options.lines,
format = options.format || 'json',
@@ -497,11 +415,11 @@ function getServiceLogs(id, options, callback) {
let cmd, args = [];
// docker and unbound use journald
if (name === 'docker' || name === 'unbound') {
if (serviceName === 'docker' || serviceName === 'unbound') {
cmd = 'journalctl';
args.push('--lines=' + (lines === -1 ? 'all' : lines));
args.push(`--unit=${name}`);
args.push(`--unit=${serviceName}`);
args.push('--no-pager');
args.push('--output=short-iso');
@@ -511,8 +429,7 @@ function getServiceLogs(id, options, callback) {
args.push('--lines=' + (lines === -1 ? '+1' : 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
const containerName = APP_SERVICES[name] ? `${name}-${instance}` : name;
args.push(path.join(paths.LOG_DIR, containerName, 'app.log'));
args.push(path.join(paths.LOG_DIR, serviceName, 'app.log'));
}
var cp = spawn(cmd, args);
@@ -531,7 +448,7 @@ function getServiceLogs(id, options, callback) {
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: message,
source: name
source: serviceName
}) + '\n';
});
@@ -542,67 +459,23 @@ function getServiceLogs(id, options, callback) {
callback(null, transformStream);
}
function restartService(id, callback) {
assert.strictEqual(typeof id, 'string');
function restartService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
const [name, instance ] = id.split(':');
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
if (instance) {
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
APP_SERVICES[name].restart(instance, callback);
} else if (SERVICES[name]) {
SERVICES[name].restart(callback);
} else {
return callback(new BoxError(BoxError.NOT_FOUND));
}
KNOWN_SERVICES[serviceName].restart(callback);
}
// in the future, we can refcount and lazy start global services
function startAppServices(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const instance = app.id;
async.eachSeries(Object.keys(app.manifest.addons || {}), function (addon, iteratorDone) {
if (!(addon in APP_SERVICES)) return iteratorDone();
APP_SERVICES[addon].start(instance, function (error) { // assume addons name is service name
// error ignored because we don't want "start app" to error. use can fix it from Services
if (error) debug(`startAppServices: ${addon}:${instance}`, error);
iteratorDone();
});
}, callback);
}
// in the future, we can refcount and stop global services as well
function stopAppServices(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const instance = app.id;
async.eachSeries(Object.keys(app.manifest.addons || {}), function (addon, iteratorDone) {
if (!(addon in APP_SERVICES)) return iteratorDone();
APP_SERVICES[addon].stop(instance, function (error) { // assume addons name is service name
// error ignored because we don't want "start app" to error. use can fix it from Services
if (error) debug(`stopAppServices: ${addon}:${instance}`, error);
iteratorDone();
});
}, callback);
}
function waitForContainer(containerName, tokenEnvName, callback) {
function waitForService(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`Waiting for ${containerName}`);
getContainerDetails(containerName, tokenEnvName, function (error, result) {
getServiceDetails(containerName, tokenEnvName, function (error, result) {
if (error) return callback(error);
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
@@ -626,11 +499,11 @@ function setupAddons(app, addons, callback) {
debugApp(app, 'setupAddons: Setting up %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
debugApp(app, 'Setting up addon %s with options %j', addon, addons[addon]);
ADDONS[addon].setup(app, addons[addon], iteratorCallback);
KNOWN_ADDONS[addon].setup(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -644,11 +517,11 @@ function teardownAddons(app, addons, callback) {
debugApp(app, 'teardownAddons: Tearing down %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
debugApp(app, 'Tearing down addon %s with options %j', addon, addons[addon]);
ADDONS[addon].teardown(app, addons[addon], iteratorCallback);
KNOWN_ADDONS[addon].teardown(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -664,9 +537,9 @@ function backupAddons(app, addons, callback) {
debugApp(app, 'backupAddons: Backing up %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
ADDONS[addon].backup(app, addons[addon], iteratorCallback);
KNOWN_ADDONS[addon].backup(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -682,9 +555,9 @@ function clearAddons(app, addons, callback) {
debugApp(app, 'clearAddons: clearing %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
ADDONS[addon].clear(app, addons[addon], iteratorCallback);
KNOWN_ADDONS[addon].clear(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -700,9 +573,9 @@ function restoreAddons(app, addons, callback) {
debugApp(app, 'restoreAddons: restoring %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
ADDONS[addon].restore(app, addons[addon], iteratorCallback);
KNOWN_ADDONS[addon].restore(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -711,12 +584,12 @@ function importAppDatabase(app, addon, callback) {
assert.strictEqual(typeof addon, 'string');
assert.strictEqual(typeof callback, 'function');
if (!(addon in ADDONS)) return callback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
if (!(addon in KNOWN_ADDONS)) return callback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
async.series([
ADDONS[addon].setup.bind(null, app, app.manifest.addons[addon]),
ADDONS[addon].clear.bind(null, app, app.manifest.addons[addon]), // clear in case we crashed in a restore
ADDONS[addon].restore.bind(null, app, app.manifest.addons[addon])
KNOWN_ADDONS[addon].setup.bind(null, app, app.manifest.addons[addon]),
KNOWN_ADDONS[addon].clear.bind(null, app, app.manifest.addons[addon]), // clear in case we crashed in a restore
KNOWN_ADDONS[addon].restore.bind(null, app, app.manifest.addons[addon])
], callback);
}
@@ -758,7 +631,7 @@ function updateServiceConfig(platformConfig, callback) {
memory = containerConfig.memory;
memorySwap = containerConfig.memorySwap;
} else {
memory = SERVICES[serviceName].defaultMemoryLimit;
memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
memorySwap = memory * 2;
}
@@ -767,28 +640,6 @@ function updateServiceConfig(platformConfig, callback) {
}, callback);
}
function updateAppServiceConfig(name, instance, servicesConfig, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof instance, 'string');
assert.strictEqual(typeof servicesConfig, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`updateAppServiceConfig: ${name}-${instance} ${JSON.stringify(servicesConfig)}`);
const serviceConfig = servicesConfig[name];
let memory, memorySwap;
if (serviceConfig && serviceConfig.memory && serviceConfig.memorySwap) {
memory = serviceConfig.memory;
memorySwap = serviceConfig.memorySwap;
} else {
memory = APP_SERVICES[name].defaultMemoryLimit;
memorySwap = memory * 2;
}
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${name}-${instance}`.split(' ');
shell.spawn(`updateAppServiceConfig${name}`, '/usr/bin/docker', args, { }, callback);
}
function startServices(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -799,7 +650,6 @@ function startServices(existingInfra, callback) {
if (existingInfra.version !== infra.version) {
debug(`startServices: ${existingInfra.version} -> ${infra.version}. starting all services`);
startFuncs.push(
startTurn.bind(null, existingInfra),
startMysql.bind(null, existingInfra),
startPostgresql.bind(null, existingInfra),
startMongodb.bind(null, existingInfra),
@@ -808,7 +658,6 @@ function startServices(existingInfra, callback) {
} else {
assert.strictEqual(typeof existingInfra.images, 'object');
if (!existingInfra.images.turn || infra.images.turn.tag !== existingInfra.images.turn.tag) startFuncs.push(startTurn.bind(null, existingInfra));
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql.bind(null, existingInfra));
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql.bind(null, existingInfra));
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb.bind(null, existingInfra));
@@ -828,7 +677,7 @@ function getEnvironment(app, callback) {
appdb.getAddonConfigByAppId(app.id, function (error, result) {
if (error) return callback(error);
if (app.manifest.addons['docker']) result.push({ name: 'CLOUDRON_DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` });
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` });
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
});
@@ -891,8 +740,8 @@ function setupLocalStorage(app, options, callback) {
// reomve any existing volume in case it's bound with an old dataDir
async.series([
docker.removeVolume.bind(null, `${app.id}-localstorage`),
docker.createVolume.bind(null, `${app.id}-localstorage`, volumeDataDir, { fqdn: app.fqdn, appId: app.id })
docker.removeVolume.bind(null, app, `${app.id}-localstorage`),
docker.createVolume.bind(null, app, `${app.id}-localstorage`, volumeDataDir)
], callback);
}
@@ -903,7 +752,7 @@ function clearLocalStorage(app, options, callback) {
debugApp(app, 'clearLocalStorage');
docker.clearVolume(`${app.id}-localstorage`, { removeDirectory: false }, callback);
docker.clearVolume(app, `${app.id}-localstorage`, { removeDirectory: false }, callback);
}
function teardownLocalStorage(app, options, callback) {
@@ -914,42 +763,35 @@ function teardownLocalStorage(app, options, callback) {
debugApp(app, 'teardownLocalStorage');
async.series([
docker.clearVolume.bind(null, `${app.id}-localstorage`, { removeDirectory: true }),
docker.removeVolume.bind(null, `${app.id}-localstorage`)
docker.clearVolume.bind(null, app, `${app.id}-localstorage`, { removeDirectory: true }),
docker.removeVolume.bind(null, app, `${app.id}-localstorage`)
], callback);
}
function setupTurn(app, options, callback) {
function setupOauth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8');
if (!turnSecret) console.error('No turn secret set. Will leave emtpy, but this is a problem!');
debugApp(app, 'setupOauth');
const env = [
{ name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() },
{ name: 'CLOUDRON_STUN_PORT', value: '3478' },
{ name: 'CLOUDRON_STUN_TLS_PORT', value: '5349' },
{ name: 'CLOUDRON_TURN_SERVER', value: settings.adminFqdn() },
{ name: 'CLOUDRON_TURN_PORT', value: '3478' },
{ name: 'CLOUDRON_TURN_TLS_PORT', value: '5349' },
{ name: 'CLOUDRON_TURN_SECRET', value: turnSecret }
];
if (!app.sso) return callback(null);
debugApp(app, 'Setting up TURN');
const env = [];
appdb.setAddonConfig(app.id, 'turn', env, callback);
debugApp(app, 'Setting oauth addon config to %j', env);
appdb.setAddonConfig(app.id, 'oauth', env, callback);
}
function teardownTurn(app, options, callback) {
function teardownOauth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Tearing down TURN');
debugApp(app, 'teardownOauth');
appdb.unsetAddonConfig(app.id, 'turn', callback);
appdb.unsetAddonConfig(app.id, 'oauth', callback);
}
function setupEmail(app, options, callback) {
@@ -1151,7 +993,7 @@ function startMysql(existingInfra, callback) {
shell.exec('startMysql', cmd, function (error) {
if (error) return callback(error);
waitForContainer('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
waitForService('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
@@ -1180,7 +1022,7 @@ function setupMySql(app, options, callback) {
password: error ? hat(4 * 48) : existingPassword // see box#362 for password length
};
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -1219,7 +1061,7 @@ function clearMySql(app, options, callback) {
const database = mysqlDatabaseName(app.id);
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1239,7 +1081,7 @@ function teardownMySql(app, options, callback) {
const database = mysqlDatabaseName(app.id);
const username = database;
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1287,7 +1129,7 @@ function backupMySql(app, options, callback) {
debugApp(app, 'Backing up mysql');
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/backup?access_token=${result.token}`;
@@ -1306,7 +1148,7 @@ function restoreMySql(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
var input = fs.createReadStream(dumpPath('mysql', app.id));
@@ -1367,7 +1209,7 @@ function startPostgresql(existingInfra, callback) {
shell.exec('startPostgresql', cmd, function (error) {
if (error) return callback(error);
waitForContainer('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
waitForService('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
@@ -1395,7 +1237,7 @@ function setupPostgreSql(app, options, callback) {
password: error ? hat(4 * 128) : existingPassword
};
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_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) {
@@ -1429,7 +1271,7 @@ function clearPostgreSql(app, options, callback) {
debugApp(app, 'Clearing postgresql');
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1448,7 +1290,7 @@ function teardownPostgreSql(app, options, callback) {
const { database, username } = postgreSqlNames(app.id);
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1469,7 +1311,7 @@ function backupPostgreSql(app, options, callback) {
const { database } = postgreSqlNames(app.id);
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`;
@@ -1488,7 +1330,7 @@ function restorePostgreSql(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
var input = fs.createReadStream(dumpPath('postgresql', app.id));
@@ -1505,43 +1347,6 @@ function restorePostgreSql(app, options, callback) {
});
}
function startTurn(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
// get and ensure we have a turn secret
var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8');
if (!turnSecret) {
turnSecret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter
safe.fs.writeFileSync(paths.ADDON_TURN_SECRET_FILE, turnSecret, 'utf8');
}
const tag = infra.images.turn.tag;
const memoryLimit = 256;
const realm = settings.adminFqdn();
if (existingInfra.version === infra.version && existingInfra.images.turn && infra.images.turn.tag === existingInfra.images.turn.tag) return callback();
// this exports 3478/tcp, 5349/tls and 50000-51000/udp
const cmd = `docker run --restart=always -d --name="turn" \
--hostname turn \
--net host \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=turn \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_TURN_SECRET="${turnSecret}" \
-e CLOUDRON_REALM="${realm}" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.exec('startTurn', cmd, callback);
}
function startMongodb(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1581,7 +1386,7 @@ function startMongodb(existingInfra, callback) {
shell.exec('startMongodb', cmd, function (error) {
if (error) return callback(error);
waitForContainer('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
waitForService('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
@@ -1608,7 +1413,7 @@ function setupMongoDb(app, options, callback) {
oplog: !!options.oplog
};
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getServiceDetails('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) {
@@ -1644,7 +1449,7 @@ function clearMongodb(app, options, callback) {
debugApp(app, 'Clearing mongodb');
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
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) {
@@ -1663,7 +1468,7 @@ function teardownMongoDb(app, options, callback) {
debugApp(app, 'Tearing down mongodb');
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
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) {
@@ -1682,7 +1487,7 @@ function backupMongoDb(app, options, callback) {
debugApp(app, 'Backing up mongodb');
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/databases/${app.id}/backup?access_token=${result.token}`;
@@ -1699,7 +1504,7 @@ function restoreMongoDb(app, options, callback) {
debugApp(app, 'restoreMongoDb');
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
@@ -1723,10 +1528,10 @@ function startRedis(existingInfra, callback) {
const tag = infra.images.redis.tag;
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.redis.tag, tag);
apps.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 (!('redis' in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
setupRedis(app, app.manifest.addons.redis, iteratorCallback);
@@ -1755,7 +1560,15 @@ function setupRedis(app, options, callback) {
const redisServiceToken = hat(4 * 48);
// Compute redis memory limit based on app's memory limit (this is arbitrary)
const memoryLimit = app.servicesConfig['redis'] ? app.servicesConfig['redis'].memoryLimit : APP_SERVICES['redis'].defaultMemoryLimit;
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
if (memoryLimit === -1) { // unrestricted (debug mode)
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
memoryLimit = 150 * 1024 * 1024; // 150m
} else {
memoryLimit = 600 * 1024 * 1024; // 600m
}
const tag = infra.images.redis.tag;
const label = app.fqdn;
@@ -1799,7 +1612,7 @@ function setupRedis(app, options, callback) {
});
},
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
waitForContainer.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
@@ -1814,7 +1627,7 @@ function clearRedis(app, options, callback) {
debugApp(app, 'Clearing redis');
getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1853,7 +1666,7 @@ function backupRedis(app, options, callback) {
debugApp(app, 'Backing up redis');
getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/backup?access_token=${result.token}`;
@@ -1870,7 +1683,7 @@ function restoreRedis(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams
getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
let input;
@@ -1893,27 +1706,6 @@ function restoreRedis(app, options, callback) {
});
}
function statusTurn(callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect('turn', function (error, container) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(error);
docker.memoryUsage(container.Id, function (error, result) {
if (error) return callback(error);
var tmp = {
status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
memoryUsed: result.memory_stats.usage,
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit)
};
callback(null, tmp);
});
});
}
function statusDocker(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -2008,13 +1800,3 @@ function statusGraphite(callback) {
});
});
}
function teardownOauth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'teardownOauth');
appdb.unsetAddonConfig(app.id, 'oauth', callback);
}
+2 -10
View File
@@ -41,7 +41,7 @@ var assert = require('assert'),
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.bindsJson',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson',
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
@@ -94,14 +94,6 @@ function postProcess(result) {
result.debugMode = safe.JSON.parse(result.debugModeJson);
delete result.debugModeJson;
assert(result.servicesConfigJson === null || typeof result.servicesConfigJson === 'string');
result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {};
delete result.servicesConfigJson;
assert(result.bindsJson === null || typeof result.bindsJson === 'string');
result.binds = safe.JSON.parse(result.bindsJson) || {};
delete result.bindsJson;
result.alternateDomains = result.alternateDomains || [];
result.alternateDomains.forEach(function (d) {
delete d.appId;
@@ -435,7 +427,7 @@ function updateWithConstraints(id, app, constraints, callback) {
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'binds') {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
+10 -5
View File
@@ -73,6 +73,7 @@ function checkAppHealth(app, callback) {
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) {
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
return callback(null);
}
@@ -102,8 +103,10 @@ function checkAppHealth(app, callback) {
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
if (error && !error.response) {
debugApp(app, 'not alive (network error): %s', error.message);
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, apps.HEALTH_HEALTHY, callback);
@@ -177,16 +180,18 @@ function processDockerEvents(intervalSecs, callback) {
function processApp(callback) {
assert.strictEqual(typeof callback, 'function');
apps.getAll(function (error, allApps) {
apps.getAll(function (error, result) {
if (error) return callback(error);
async.each(allApps, checkAppHealth, function (error) {
async.each(result, checkAppHealth, function (error) {
if (error) console.error(error);
const alive = allApps
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; });
const alive = result
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; })
.map(a => a.fqdn)
.join(', ');
debug(`app health: ${alive.length} alive / ${allApps.length - alive.length} dead`);
debug('apps alive: [%s]', alive);
callback(null);
});
+898 -925
View File
File diff suppressed because it is too large Load Diff
+8 -23
View File
@@ -127,7 +127,7 @@ function registerUser(email, password, callback) {
const url = settings.apiServerOrigin() + '/api/v1/register_user';
superagent.post(url).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 === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
if (result.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
callback(null);
@@ -340,8 +340,7 @@ function sendAliveStatus(callback) {
});
}
function getBoxUpdate(options, callback) {
assert.strictEqual(typeof options, 'object');
function getBoxUpdate(callback) {
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
@@ -349,13 +348,7 @@ function getBoxUpdate(options, callback) {
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
const query = {
accessToken: token,
boxVersion: constants.VERSION,
automatic: options.automatic
};
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION }).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));
@@ -381,24 +374,16 @@ function getBoxUpdate(options, callback) {
});
}
function getAppUpdate(app, options, callback) {
function getAppUpdate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
const query = {
accessToken: token,
boxVersion: constants.VERSION,
appId: app.appStoreId,
appVersion: app.manifest.version,
automatic: options.automatic
};
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
@@ -430,7 +415,7 @@ function registerCloudron(data, callback) {
superagent.post(url).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 !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${result.statusCode} ${error.message}`));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${error.message}`));
// cloudronId, token, licenseKey
if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
@@ -486,7 +471,7 @@ function registerWithLicense(license, domain, callback) {
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'));
if (token) return callback(new BoxError(BoxError.CONFLICT));
const provider = settings.provider();
const version = constants.VERSION;
@@ -506,7 +491,7 @@ function registerWithLoginCredentials(options, callback) {
}
getCloudronToken(function (error, token) {
if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'));
if (token) return callback(new BoxError(BoxError.CONFLICT));
maybeSignup(function (error) {
if (error) return callback(error);
+1 -7
View File
@@ -899,10 +899,7 @@ function start(app, args, progressCallback, callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
progressCallback.bind(null, { percent: 10, message: 'Starting app services' }),
addons.startAppServices.bind(null, app),
progressCallback.bind(null, { percent: 35, message: 'Starting container' }),
progressCallback.bind(null, { percent: 20, message: 'Starting container' }),
docker.startContainer.bind(null, app.id),
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
@@ -930,9 +927,6 @@ function stop(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 20, message: 'Stopping container' }),
docker.stopContainers.bind(null, app.id),
progressCallback.bind(null, { percent: 50, message: 'Stopping app services' }),
addons.stopAppServices.bind(null, app),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
+4 -5
View File
@@ -6,7 +6,7 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'packageVersion', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion' ];
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs' ];
exports = module.exports = {
add: add,
@@ -106,8 +106,7 @@ function get(id, callback) {
function add(id, data, callback) {
assert(data && typeof data === 'object');
assert.strictEqual(typeof id, 'string');
assert(data.encryptionVersion === null || typeof data.encryptionVersion === 'number');
assert.strictEqual(typeof data.packageVersion, 'string');
assert.strictEqual(typeof data.version, 'string');
assert(data.type === exports.BACKUP_TYPE_APP || data.type === exports.BACKUP_TYPE_BOX);
assert(util.isArray(data.dependsOn));
assert.strictEqual(typeof data.manifest, 'object');
@@ -117,8 +116,8 @@ function add(id, data, callback) {
var creationTime = data.creationTime || new Date(); // allow tests to set the time
var manifestJson = JSON.stringify(data.manifest);
database.query('INSERT INTO backups (id, encryptionVersion, packageVersion, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
[ id, data.encryptionVersion, data.packageVersion, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ],
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[ id, data.version, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ],
function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
+119 -352
View File
@@ -32,13 +32,12 @@ exports = module.exports = {
configureCollectd: configureCollectd,
generateEncryptionKeysSync: generateEncryptionKeysSync,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
// for testing
_getBackupFilePath: getBackupFilePath,
_restoreFsMetadata: restoreFsMetadata,
_saveFsMetadata: saveFsMetadata,
_applyBackupRetentionPolicy: applyBackupRetentionPolicy
_saveFsMetadata: saveFsMetadata
};
var addons = require('./addons.js'),
@@ -58,7 +57,6 @@ var addons = require('./addons.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
locker = require('./locker.js'),
moment = require('moment'),
mkdirp = require('mkdirp'),
once = require('once'),
path = require('path'),
@@ -71,7 +69,6 @@ var addons = require('./addons.js'),
syncer = require('./syncer.js'),
tar = require('tar-fs'),
tasks = require('./tasks.js'),
TransformStream = require('stream').Transform,
util = require('util'),
zlib = require('zlib');
@@ -97,30 +94,19 @@ function api(provider) {
case 'wasabi': return require('./storage/s3.js');
case 'scaleway-objectstorage': return require('./storage/s3.js');
case 'linode-objectstorage': return require('./storage/s3.js');
case 'ovh-objectstorage': return require('./storage/s3.js');
case 'noop': return require('./storage/noop.js');
default: return null;
}
}
function injectPrivateFields(newConfig, currentConfig) {
if ('password' in newConfig) {
if (newConfig.password === constants.SECRET_PLACEHOLDER) {
delete newConfig.password;
}
newConfig.encryption = currentConfig.encryption || null;
} else {
newConfig.encryption = null;
}
if (newConfig.key === exports.SECRET_PLACEHOLDER) newConfig.key = currentConfig.key;
if (newConfig.provider === currentConfig.provider) api(newConfig.provider).injectPrivateFields(newConfig, currentConfig);
}
function removePrivateFields(backupConfig) {
assert.strictEqual(typeof backupConfig, 'object');
if (backupConfig.encryption) {
delete backupConfig.encryption;
backupConfig.password = constants.SECRET_PLACEHOLDER;
}
if (backupConfig.key) backupConfig.key = exports.SECRET_PLACEHOLDER;
return api(backupConfig.provider).removePrivateFields(backupConfig);
}
@@ -134,25 +120,12 @@ function testConfig(backupConfig, callback) {
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BoxError(BoxError.BAD_FIELD, 'unknown format', { field: 'format' }));
// 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' }));
if (backupConfig.password.length < 8) return callback(new BoxError(BoxError.BAD_FIELD, 'password must be atleast 8 characters', { field: 'password' }));
}
const policy = backupConfig.retentionPolicy;
if (!policy) return callback(new BoxError(BoxError.BAD_FIELD, 'retentionPolicy is required', { field: 'retentionPolicy' }));
if ('keepWithinSecs' in policy && typeof policy.keepWithinSecs !== 'number') return callback(new BoxError(BoxError.BAD_FIELD, 'keepWithinSecs must be a number', { field: 'retentionPolicy' }));
if ('keepDaily' in policy && typeof policy.keepDaily !== 'number') return callback(new BoxError(BoxError.BAD_FIELD, 'keepDaily must be a number', { field: 'retentionPolicy' }));
if ('keepWeekly' in policy && typeof policy.keepWeekly !== 'number') return callback(new BoxError(BoxError.BAD_FIELD, 'keepWeekly must be a number', { field: 'retentionPolicy' }));
if ('keepMonthly' in policy && typeof policy.keepMonthly !== 'number') return callback(new BoxError(BoxError.BAD_FIELD, 'keepMonthly must be a number', { field: 'retentionPolicy' }));
if ('keepYearly' in policy && typeof policy.keepYearly !== 'number') return callback(new BoxError(BoxError.BAD_FIELD, 'keepYearly must be a number', { field: 'retentionPolicy' }));
if (backupConfig.intervalSecs < 6 * 60 * 60) return callback(new BoxError(BoxError.BAD_FIELD, 'Interval must be atleast 6 hours', { field: 'interval' }));
api(backupConfig.provider).testConfig(backupConfig, callback);
}
// this skips password check since that policy is only at creation time
function testProviderConfig(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -163,18 +136,6 @@ function testProviderConfig(backupConfig, callback) {
api(backupConfig.provider).testConfig(backupConfig, callback);
}
function generateEncryptionKeysSync(password) {
assert.strictEqual(typeof password, 'string');
const aesKeys = crypto.scryptSync(password, Buffer.from('CLOUDRONSCRYPTSALT', 'utf8'), 128);
return {
dataKey: aesKeys.slice(0, 32).toString('hex'),
dataHmacKey: aesKeys.slice(32, 64).toString('hex'),
filenameKey: aesKeys.slice(64, 96).toString('hex'),
filenameHmacKey: aesKeys.slice(96).toString('hex')
};
}
function getByStatePaged(state, page, perPage, callback) {
assert.strictEqual(typeof state, 'string');
assert(typeof page === 'number' && page > 0);
@@ -218,23 +179,21 @@ function getBackupFilePath(backupConfig, backupId, format) {
assert.strictEqual(typeof format, 'string');
if (format === 'tgz') {
const fileType = backupConfig.encryption ? '.tar.gz.enc' : '.tar.gz';
const fileType = backupConfig.key ? '.tar.gz.enc' : '.tar.gz';
return path.join(backupConfig.prefix || backupConfig.backupFolder || '', backupId+fileType);
} else {
return path.join(backupConfig.prefix || backupConfig.backupFolder || '', backupId);
}
}
function encryptFilePath(filePath, encryption) {
function encryptFilePath(filePath, key) {
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof encryption, 'object');
assert.strictEqual(typeof key, 'string');
var encryptedParts = filePath.split('/').map(function (part) {
let hmac = crypto.createHmac('sha256', Buffer.from(encryption.filenameHmacKey, 'hex'));
const iv = hmac.update(part).digest().slice(0, 16); // iv has to be deterministic, for our sync (copy) logic to work
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(encryption.filenameKey, 'hex'), iv);
const cipher = crypto.createCipher('aes-256-cbc', key);
let crypt = cipher.update(part);
crypt = Buffer.concat([ iv, crypt, cipher.final() ]);
crypt = Buffer.concat([ crypt, cipher.final() ]);
return crypt.toString('base64') // ensures path is valid
.replace(/\//g, '-') // replace '/' of base64 since it conflicts with path separator
@@ -244,9 +203,9 @@ function encryptFilePath(filePath, encryption) {
return encryptedParts.join('/');
}
function decryptFilePath(filePath, encryption) {
function decryptFilePath(filePath, key) {
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof encryption, 'object');
assert.strictEqual(typeof key, 'string');
let decryptedParts = [];
for (let part of filePath.split('/')) {
@@ -254,172 +213,61 @@ function decryptFilePath(filePath, encryption) {
part = part.replace(/-/g, '/'); // replace with '/'
try {
const buffer = Buffer.from(part, 'base64');
const iv = buffer.slice(0, 16);
let decrypt = crypto.createDecipheriv('aes-256-cbc', Buffer.from(encryption.filenameKey, 'hex'), iv);
const plainText = decrypt.update(buffer.slice(16));
const plainTextString = Buffer.concat([ plainText, decrypt.final() ]).toString('utf8');
const hmac = crypto.createHmac('sha256', Buffer.from(encryption.filenameHmacKey, 'hex'));
if (!hmac.update(plainTextString).digest().slice(0, 16).equals(iv)) return { error: new BoxError(BoxError.CRYPTO_ERROR, `mac error decrypting part ${part} of path ${filePath}`) };
decryptedParts.push(plainTextString);
let decrypt = crypto.createDecipher('aes-256-cbc', key);
let text = decrypt.update(Buffer.from(part, 'base64'));
text = Buffer.concat([ text, decrypt.final() ]);
decryptedParts.push(text.toString('utf8'));
} catch (error) {
debug(`Error decrypting part ${part} of path ${filePath}:`, error);
return { error: new BoxError(BoxError.CRYPTO_ERROR, `Error decrypting part ${part} of path ${filePath}: ${error.message}`) };
debug(`Error decrypting file ${filePath} part ${part}:`, error);
return null;
}
}
return { result: decryptedParts.join('/') };
return decryptedParts.join('/');
}
class EncryptStream extends TransformStream {
constructor(encryption) {
super();
this._headerPushed = false;
this._iv = crypto.randomBytes(16);
this._cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(encryption.dataKey, 'hex'), this._iv);
this._hmac = crypto.createHmac('sha256', Buffer.from(encryption.dataHmacKey, 'hex'));
}
pushHeaderIfNeeded() {
if (!this._headerPushed) {
const magic = Buffer.from('CBV2');
this.push(magic);
this._hmac.update(magic);
this.push(this._iv);
this._hmac.update(this._iv);
this._headerPushed = true;
}
}
_transform(chunk, ignoredEncoding, callback) {
this.pushHeaderIfNeeded();
try {
const crypt = this._cipher.update(chunk);
this._hmac.update(crypt);
callback(null, crypt);
} catch (error) {
callback(error);
}
}
_flush(callback) {
try {
this.pushHeaderIfNeeded(); // for 0-length files
const crypt = this._cipher.final();
this.push(crypt);
this._hmac.update(crypt);
callback(null, this._hmac.digest()); // +32 bytes
} catch (error) {
callback(error);
}
}
}
class DecryptStream extends TransformStream {
constructor(encryption) {
super();
this._key = Buffer.from(encryption.dataKey, 'hex');
this._header = Buffer.alloc(0);
this._decipher = null;
this._hmac = crypto.createHmac('sha256', Buffer.from(encryption.dataHmacKey, 'hex'));
this._buffer = Buffer.alloc(0);
}
_transform(chunk, ignoredEncoding, callback) {
const needed = 20 - this._header.length; // 4 for magic, 16 for iv
if (this._header.length !== 20) { // not gotten header yet
this._header = Buffer.concat([this._header, chunk.slice(0, needed)]);
if (this._header.length !== 20) return callback();
if (!this._header.slice(0, 4).equals(new Buffer.from('CBV2'))) return callback(new BoxError(BoxError.CRYPTO_ERROR, 'Invalid magic in header'));
const iv = this._header.slice(4);
this._decipher = crypto.createDecipheriv('aes-256-cbc', this._key, iv);
this._hmac.update(this._header);
}
this._buffer = Buffer.concat([ this._buffer, chunk.slice(needed) ]);
if (this._buffer.length < 32) return callback(); // hmac trailer length is 32
try {
const cipherText = this._buffer.slice(0, -32);
this._hmac.update(cipherText);
const plainText = this._decipher.update(cipherText);
this._buffer = this._buffer.slice(-32);
callback(null, plainText);
} catch (error) {
callback(error);
}
}
_flush (callback) {
if (this._buffer.length !== 32) return callback(new BoxError(BoxError.CRYPTO_ERROR, 'Invalid password or tampered file (not enough data)'));
try {
if (!this._hmac.digest().equals(this._buffer)) return callback(new BoxError(BoxError.CRYPTO_ERROR, 'Invalid password or tampered file (mac mismatch)'));
const plainText = this._decipher.final();
callback(null, plainText);
} catch (error) {
callback(error);
}
}
}
function createReadStream(sourceFile, encryption) {
function createReadStream(sourceFile, key) {
assert.strictEqual(typeof sourceFile, 'string');
assert.strictEqual(typeof encryption, 'object');
assert(key === null || typeof key === 'string');
var stream = fs.createReadStream(sourceFile);
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
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}`));
debug('createReadStream: read stream error.', error);
ps.emit('error', new BoxError(BoxError.FS_ERROR, error.message));
});
if (encryption) {
let encryptStream = new EncryptStream(encryption);
encryptStream.on('error', function (error) {
debug(`createReadStream: encrypt stream error ${sourceFile}`, error);
ps.emit('error', new BoxError(BoxError.CRYPTO_ERROR, `Encryption error at ${sourceFile}: ${error.message}`));
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
debug('createReadStream: encrypt stream error.', error);
ps.emit('error', new BoxError(BoxError.CRYPTO_ERROR, error.message));
});
return stream.pipe(encryptStream).pipe(ps);
return stream.pipe(encrypt).pipe(ps);
} else {
return stream.pipe(ps);
}
}
function createWriteStream(destFile, encryption) {
function createWriteStream(destFile, key) {
assert.strictEqual(typeof destFile, 'string');
assert.strictEqual(typeof encryption, 'object');
assert(key === null || typeof key === 'string');
var stream = fs.createWriteStream(destFile);
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
stream.on('error', function (error) {
debug(`createWriteStream: write stream error ${destFile}`, error);
ps.emit('error', new BoxError(BoxError.FS_ERROR, `Write error ${destFile}: ${error.message}`));
debug('createWriteStream: write stream error.', error);
ps.emit('error', new BoxError(BoxError.FS_ERROR, error.message));
});
stream.on('finish', function () {
debug('createWriteStream: done.');
// we use a separate event because ps is a through2 stream which emits 'finish' event indicating end of inStream and not write
ps.emit('done');
});
if (encryption) {
let decrypt = new DecryptStream(encryption);
if (key !== null) {
var decrypt = crypto.createDecipher('aes-256-cbc', key);
decrypt.on('error', function (error) {
debug(`createWriteStream: decrypt stream error ${destFile}`, error);
ps.emit('error', new BoxError(BoxError.CRYPTO_ERROR, `Decryption error at ${destFile}: ${error.message}`));
debug('createWriteStream: decrypt stream error.', error);
ps.emit('error', new BoxError(BoxError.CRYPTO_ERROR, error.message));
});
ps.pipe(decrypt).pipe(stream);
} else {
ps.pipe(stream);
@@ -428,9 +276,9 @@ function createWriteStream(destFile, encryption) {
return ps;
}
function tarPack(dataLayout, encryption, callback) {
function tarPack(dataLayout, key, callback) {
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof encryption, 'object');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof callback, 'function');
var pack = tar.pack('/', {
@@ -451,7 +299,7 @@ function tarPack(dataLayout, encryption, callback) {
});
var gzip = zlib.createGzip({});
var ps = progressStream({ time: 10000 }); // emit 'progress' every 10 seconds
var ps = progressStream({ time: 10000 }); // emit 'pgoress' every 10 seconds
pack.on('error', function (error) {
debug('tarPack: tar stream error.', error);
@@ -463,19 +311,18 @@ function tarPack(dataLayout, encryption, callback) {
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
if (encryption) {
const encryptStream = new EncryptStream(encryption);
encryptStream.on('error', function (error) {
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
debug('tarPack: encrypt stream error.', error);
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
pack.pipe(gzip).pipe(encryptStream).pipe(ps);
pack.pipe(gzip).pipe(encrypt).pipe(ps);
} else {
pack.pipe(gzip).pipe(ps);
}
return callback(null, ps);
callback(null, ps);
}
function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
@@ -491,7 +338,7 @@ function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
syncer.sync(dataLayout, function processTask(task, iteratorCallback) {
debug('sync: processing task: %j', task);
// the empty task.path is special to signify the directory
const destPath = task.path && backupConfig.encryption ? encryptFilePath(task.path, backupConfig.encryption) : task.path;
const destPath = task.path && backupConfig.key ? encryptFilePath(task.path, backupConfig.key) : task.path;
const backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), destPath);
if (task.operation === 'removedir') {
@@ -512,15 +359,15 @@ function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
if (task.operation === 'add') {
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);
var stream = createReadStream(dataLayout.toLocalPath('./' + task.path), backupConfig.key || null);
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
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
});
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
debug(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
@@ -613,13 +460,13 @@ function upload(backupId, format, dataLayoutString, progressCallback, callback)
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
tarPack(dataLayout, backupConfig.encryption, function (error, tarStream) {
tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) {
if (error) return retryCallback(error);
tarStream.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 backup' }); // 0M@0MBps looks wrong
progressCallback({ message: `Uploading backup ${transferred}M@${speed}MBps` });
if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Uploading backup ${transferred}M@${speed}Mbps` });
});
tarStream.on('error', retryCallback); // already returns BoxError
@@ -636,10 +483,10 @@ function upload(backupId, format, dataLayoutString, progressCallback, callback)
});
}
function tarExtract(inStream, dataLayout, encryption, callback) {
function tarExtract(inStream, dataLayout, key, callback) {
assert.strictEqual(typeof inStream, 'object');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof encryption, 'object');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof callback, 'function');
var gunzip = zlib.createGunzip({});
@@ -648,14 +495,10 @@ function tarExtract(inStream, dataLayout, encryption, callback) {
map: function (header) {
header.name = dataLayout.toLocalPath(header.name);
return header;
},
dmode: 500 // ensure directory is writable
}
});
const emitError = once((error) => {
inStream.destroy();
ps.emit('error', error);
});
const emitError = once((error) => ps.emit('error', error));
inStream.on('error', function (error) {
debug('tarExtract: input stream error.', error);
@@ -678,8 +521,8 @@ function tarExtract(inStream, dataLayout, encryption, callback) {
ps.emit('done');
});
if (encryption) {
let decrypt = new DecryptStream(encryption);
if (key !== null) {
var decrypt = crypto.createDecipher('aes-256-cbc', key);
decrypt.on('error', function (error) {
debug('tarExtract: decrypt stream error.', error);
emitError(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
@@ -730,10 +573,9 @@ function downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback,
function downloadFile(entry, done) {
let relativePath = path.relative(backupFilePath, entry.fullPath);
if (backupConfig.encryption) {
const { error, result } = decryptFilePath(relativePath, backupConfig.encryption);
if (error) return done(new BoxError(BoxError.CRYPTO_ERROR, 'Unable to decrypt file'));
relativePath = result;
if (backupConfig.key) {
relativePath = decryptFilePath(relativePath, backupConfig.key);
if (!relativePath) return done(new BoxError(BoxError.BAD_STATE, 'Unable to decrypt file'));
}
const destFilePath = dataLayout.toLocalPath('./' + relativePath);
@@ -741,35 +583,31 @@ function downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback,
if (error) return done(new BoxError(BoxError.FS_ERROR, error.message));
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
destStream.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: `Downloading ${entry.fullPath}` }); // 0M@0Mbps looks wrong
progressCallback({ message: `Downloading ${entry.fullPath}: ${transferred}M@${speed}Mbps` });
});
// protect against multiple errors. must destroy the write stream so that a previous retry does not write
let closeAndRetry = once((error) => {
if (error) progressCallback({ message: `Download ${entry.fullPath} to ${destFilePath} errored: ${error.message}` });
else progressCallback({ message: `Download ${entry.fullPath} to ${destFilePath} finished` });
destStream.destroy();
retryCallback(error);
});
api(backupConfig.provider).download(backupConfig, entry.fullPath, function (error, sourceStream) {
if (error) {
progressCallback({ message: `Download ${entry.fullPath} to ${destFilePath} errored: ${error.message}` });
return retryCallback(error);
}
let destStream = createWriteStream(destFilePath, backupConfig.encryption);
// protect against multiple errors. must destroy the write stream so that a previous retry does not write
let closeAndRetry = once((error) => {
if (error) progressCallback({ message: `Download ${entry.fullPath} to ${destFilePath} errored: ${error.message}` });
else progressCallback({ message: `Download ${entry.fullPath} to ${destFilePath} finished` });
sourceStream.destroy();
destStream.destroy();
retryCallback(error);
});
destStream.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: `Downloading ${entry.fullPath}` }); // 0M@0MBps looks wrong
progressCallback({ message: `Downloading ${entry.fullPath}: ${transferred}M@${speed}MBps` });
});
destStream.on('error', closeAndRetry);
if (error) return closeAndRetry(error);
sourceStream.on('error', closeAndRetry);
destStream.on('error', closeAndRetry); // already emits BoxError
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
sourceStream.pipe(destStream, { end: true }).on('done', closeAndRetry);
sourceStream.pipe(destStream, { end: true }).on('finish', closeAndRetry);
});
}, done);
});
@@ -800,13 +638,13 @@ function download(backupConfig, backupId, format, dataLayout, progressCallback,
api(backupConfig.provider).download(backupConfig, backupFilePath, function (error, sourceStream) {
if (error) return retryCallback(error);
tarExtract(sourceStream, dataLayout, backupConfig.encryption, function (error, ps) {
tarExtract(sourceStream, dataLayout, backupConfig.key || null, function (error, ps) {
if (error) return retryCallback(error);
ps.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: 'Downloading backup' }); // 0M@0MBps looks wrong
progressCallback({ message: `Downloading ${transferred}M@${speed}MBps` });
if (!transferred && !speed) return progressCallback({ message: 'Downloading backup' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Downloading ${transferred}M@${speed}Mbps` });
});
ps.on('error', retryCallback);
ps.on('done', retryCallback);
@@ -979,15 +817,7 @@ function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, call
debug(`Rotating box backup to id ${backupId}`);
const data = {
encryptionVersion: backupConfig.encryption ? 2 : null,
packageVersion: constants.VERSION,
type: backupdb.BACKUP_TYPE_BOX,
dependsOn: appBackupIds,
manifest: null,
format: format
};
backupdb.add(backupId, data, function (error) {
backupdb.add(backupId, { version: constants.VERSION, type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
if (error) return callback(error);
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format));
@@ -1025,10 +855,9 @@ function backupBoxWithAppBackupIds(appBackupIds, tag, progressCallback, callback
}
function canBackupApp(app) {
// only backup apps that are installed or specific pending states
// we used to check the health here but that doesn't work for stopped apps. it's better to just fail
// and inform the user if the backup fails and the app addons have not been setup yet.
return app.installationState === apps.ISTATE_INSTALLED ||
// only backup apps that are installed or pending configure or called from apptask. Rest of them are in some
// state not good for consistent backup (i.e addons may not have been setup completely)
return (app.installationState === apps.ISTATE_INSTALLED && app.health === apps.HEALTH_HEALTHY) ||
app.installationState === apps.ISTATE_PENDING_CONFIGURE ||
app.installationState === apps.ISTATE_PENDING_BACKUP || // called from apptask
app.installationState === apps.ISTATE_PENDING_UPDATE; // called from apptask
@@ -1069,16 +898,7 @@ function rotateAppBackup(backupConfig, app, tag, options, progressCallback, call
debug(`Rotating app backup of ${app.id} to id ${backupId}`);
const data = {
encryptionVersion: backupConfig.encryption ? 2 : null,
packageVersion: manifest.version,
type: backupdb.BACKUP_TYPE_APP,
dependsOn: [ ],
manifest,
format: format
};
backupdb.add(backupId, data, function (error) {
backupdb.add(backupId, { version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], manifest: manifest, format: format }, function (error) {
if (error) return callback(error);
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format));
@@ -1258,57 +1078,9 @@ function ensureBackup(auditSource, callback) {
});
}
// backups must be descending in creationTime
function applyBackupRetentionPolicy(backups, policy) {
assert(Array.isArray(backups));
assert.strictEqual(typeof policy, 'object');
const now = new Date();
for (const backup of backups) {
if (backup.keepReason) continue; // already kept for some other reason
if ((now - backup.creationTime) < (backup.preserveSecs * 1000)) {
backup.keepReason = 'preserveSecs';
} else if ((now - backup.creationTime < policy.keepWithinSecs * 1000) || policy.keepWithinSecs < 0) {
backup.keepReason = 'keepWithinSecs';
}
}
const KEEP_FORMATS = {
keepDaily: 'Y-M-D',
keepWeekly: 'Y-W',
keepMonthly: 'Y-M',
keepYearly: 'Y'
};
for (const format of [ 'keepDaily', 'keepWeekly', 'keepMonthly', 'keepYearly' ]) {
if (!(format in policy)) continue;
const n = policy[format]; // we want to keep "n" backups of format
if (!n) continue; // disabled rule
let lastPeriod = null, keptSoFar = 0;
for (const backup of backups) {
if (backup.keepReason) continue; // already kept for some other reason
const period = moment(backup.creationTime).format(KEEP_FORMATS[format]);
if (period === lastPeriod) continue; // already kept for this period
lastPeriod = period;
backup.keepReason = format;
if (++keptSoFar === n) break;
}
}
for (const backup of backups) {
if (backup.keepReason) debug(`applyBackupRetentionPolicy: ${backup.id} ${backup.type} ${backup.keepReason}`);
}
}
function cleanupBackup(backupConfig, backup, progressCallback, callback) {
function cleanupBackup(backupConfig, backup, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backup, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var backupFilePath = getBackupFilePath(backupConfig, backup.id, backup.format);
@@ -1333,60 +1105,55 @@ function cleanupBackup(backupConfig, backup, progressCallback, callback) {
}
if (backup.format ==='tgz') {
progressCallback({ message: `${backup.id}: Removing ${backupFilePath}`});
api(backupConfig.provider).remove(backupConfig, backupFilePath, done);
} else {
var events = api(backupConfig.provider).removeDir(backupConfig, backupFilePath);
events.on('progress', (message) => progressCallback({ message: `${backup.id}: ${message}` }));
events.on('progress', function (detail) { debug(`cleanupBackup: ${detail}`); });
events.on('done', done);
}
}
function cleanupAppBackups(backupConfig, referencedAppBackupIds, progressCallback, callback) {
function cleanupAppBackups(backupConfig, referencedAppBackups, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert(Array.isArray(referencedAppBackupIds));
assert.strictEqual(typeof progressCallback, 'function');
assert(Array.isArray(referencedAppBackups));
assert.strictEqual(typeof callback, 'function');
let removedAppBackupIds = [];
const now = new Date();
let removedAppBackups = [];
// we clean app backups of any state because the ones to keep are determined by the box cleanup code
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_APP, 1, 1000, function (error, appBackups) {
if (error) return callback(error);
for (const appBackup of appBackups) { // set the reason so that policy filter can skip it
if (referencedAppBackupIds.includes(appBackup.id)) appBackup.keepReason = 'reference';
}
applyBackupRetentionPolicy(appBackups, backupConfig.retentionPolicy);
async.eachSeries(appBackups, function iterator(appBackup, iteratorDone) {
if (appBackup.keepReason) return iteratorDone();
if (referencedAppBackups.indexOf(appBackup.id) !== -1) return iteratorDone();
if ((now - appBackup.creationTime) < (appBackup.preserveSecs * 1000)) return iteratorDone();
if ((now - appBackup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
progressCallback({ message: `Removing app backup ${appBackup.id}`});
debug('cleanupAppBackups: removing %s', appBackup.id);
removedAppBackupIds.push(appBackup.id);
cleanupBackup(backupConfig, appBackup, progressCallback, iteratorDone);
removedAppBackups.push(appBackup.id);
cleanupBackup(backupConfig, appBackup, iteratorDone);
}, function () {
debug('cleanupAppBackups: done');
callback(null, removedAppBackupIds);
callback(null, removedAppBackups);
});
});
}
function cleanupBoxBackups(backupConfig, progressCallback, auditSource, callback) {
function cleanupBoxBackups(backupConfig, auditSource, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
let referencedAppBackupIds = [], removedBoxBackupIds = [];
const now = new Date();
let referencedAppBackups = [], removedBoxBackups = [];
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_BOX, 1, 1000, function (error, boxBackups) {
if (error) return callback(error);
if (boxBackups.length === 0) return callback(null, { removedBoxBackupIds, referencedAppBackupIds });
if (boxBackups.length === 0) return callback(null, { removedBoxBackups, referencedAppBackups });
// search for the first valid backup
var i;
@@ -1397,28 +1164,28 @@ function cleanupBoxBackups(backupConfig, progressCallback, auditSource, callback
// keep the first valid backup
if (i !== boxBackups.length) {
debug('cleanupBoxBackups: preserving box backup %s (%j)', boxBackups[i].id, boxBackups[i].dependsOn);
referencedAppBackupIds = boxBackups[i].dependsOn;
referencedAppBackups = boxBackups[i].dependsOn;
boxBackups.splice(i, 1);
} else {
debug('cleanupBoxBackups: no box backup to preserve');
}
applyBackupRetentionPolicy(boxBackups, backupConfig.retentionPolicy);
async.eachSeries(boxBackups, function iterator(boxBackup, iteratorNext) {
if (boxBackup.keepReason) {
referencedAppBackupIds = referencedAppBackupIds.concat(boxBackup.dependsOn);
// TODO: errored backups should probably be cleaned up before retention time, but we will
// have to be careful not to remove any backup currently being created
if ((now - boxBackup.creationTime) < (backupConfig.retentionSecs * 1000)) {
referencedAppBackups = referencedAppBackups.concat(boxBackup.dependsOn);
return iteratorNext();
}
progressCallback({ message: `Removing box backup ${boxBackup.id}`});
debug('cleanupBoxBackups: removing %s', boxBackup.id);
removedBoxBackupIds.push(boxBackup.id);
cleanupBackup(backupConfig, boxBackup, progressCallback, iteratorNext);
removedBoxBackups.push(boxBackup.id);
cleanupBackup(backupConfig, boxBackup, iteratorNext);
}, function () {
debug('cleanupBoxBackups: done');
callback(null, { removedBoxBackupIds, referencedAppBackupIds });
callback(null, { removedBoxBackups, referencedAppBackups });
});
});
}
@@ -1480,19 +1247,19 @@ function cleanup(auditSource, progressCallback, callback) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
if (backupConfig.retentionPolicy.keepWithinSecs < 0) {
if (backupConfig.retentionSecs < 0) {
debug('cleanup: keeping all backups');
return callback(null, {});
}
progressCallback({ percent: 10, message: 'Cleaning box backups' });
cleanupBoxBackups(backupConfig, progressCallback, auditSource, function (error, { removedBoxBackupIds, referencedAppBackupIds }) {
cleanupBoxBackups(backupConfig, auditSource, function (error, result) {
if (error) return callback(error);
progressCallback({ percent: 40, message: 'Cleaning app backups' });
cleanupAppBackups(backupConfig, referencedAppBackupIds, progressCallback, function (error, removedAppBackupIds) {
cleanupAppBackups(backupConfig, result.referencedAppBackups, function (error, removedAppBackups) {
if (error) return callback(error);
progressCallback({ percent: 90, message: 'Cleaning snapshots' });
@@ -1500,7 +1267,7 @@ function cleanup(auditSource, progressCallback, callback) {
cleanupSnapshots(backupConfig, function (error) {
if (error) return callback(error);
callback(null, { removedBoxBackupIds, removedAppBackupIds });
callback(null, { removedBoxBackups: result.removedBoxBackups, removedAppBackups: removedAppBackups });
});
});
});
+1 -1
View File
@@ -332,7 +332,7 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves
var key = safe.child_process.execSync('openssl genrsa 4096');
if (!key) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
+1 -3
View File
@@ -21,8 +21,7 @@ exports = module.exports = {
runSystemChecks: runSystemChecks,
};
var addons = require('./addons.js'),
apps = require('./apps.js'),
var apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
async = require('async'),
@@ -327,7 +326,6 @@ function setDashboardAndMailDomain(domain, auditSource, callback) {
if (error) return callback(error);
mail.onMailFqdnChanged(NOOP_CALLBACK); // this will update dns and re-configure mail server
addons.restartService('turn', NOOP_CALLBACK); // to update the realm variable
callback(null);
});
+2 -3
View File
@@ -37,11 +37,10 @@ exports = module.exports = {
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js
DEMO_USERNAME: 'cloudron',
DEMO_BLACKLISTED_APPS: [ 'com.github.cloudtorrent' ],
AUTOUPDATE_PATTERN_NEVER: 'never',
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8), // also used in dashboard client.js
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
CLOUDRON: CLOUDRON,
TEST: TEST,
@@ -50,6 +49,6 @@ exports = module.exports = {
FOOTER: '&copy; 2020 &nbsp; [Cloudron](https://cloudron.io) &nbsp; &nbsp; &nbsp; [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)',
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '5.1.1-test'
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '4.2.0-test'
};
+4 -4
View File
@@ -80,14 +80,14 @@ function startJobs(callback) {
});
gJobs.boxUpdateCheckerJob = new CronJob({
cronTime: '00 ' + randomMinute + ' 23 * * *', // once an day
onTick: () => updateChecker.checkBoxUpdates({ automatic: true }, NOOP_CALLBACK),
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: () => updateChecker.checkBoxUpdates(NOOP_CALLBACK),
start: true
});
gJobs.appUpdateChecker = new CronJob({
cronTime: '00 ' + randomMinute + ' 22 * * *', // once an day
onTick: () => updateChecker.checkAppUpdates({ automatic: true }, NOOP_CALLBACK),
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: () => updateChecker.checkAppUpdates(NOOP_CALLBACK),
start: true
});
+2 -3
View File
@@ -12,7 +12,6 @@ exports = module.exports = {
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'),
@@ -32,7 +31,7 @@ function getFqdn(location, domain) {
}
function removePrivateFields(domainObject) {
domainObject.config.token = constants.SECRET_PLACEHOLDER;
domainObject.config.token = domains.SECRET_PLACEHOLDER;
// do not return the 'key'. in caas, this is private
delete domainObject.fallbackCertificate.key;
@@ -41,7 +40,7 @@ function removePrivateFields(domainObject) {
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function upsert(domainObject, location, type, values, callback) {
+6 -12
View File
@@ -13,7 +13,6 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -26,12 +25,12 @@ var assert = require('assert'),
var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4';
function removePrivateFields(domainObject) {
domainObject.config.token = constants.SECRET_PLACEHOLDER;
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function translateRequestError(result, callback) {
@@ -40,14 +39,9 @@ function translateRequestError(result, callback) {
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
if (result.statusCode === 422) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
if (result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) {
let message = 'Unknown error';
if (typeof result.body.error === 'string') {
message = `message: ${result.body.error} statusCode: ${result.statusCode}`;
} else if (Array.isArray(result.body.errors) && result.body.errors.length > 0) {
let error = result.body.errors[0];
message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
}
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
let error = result.body.errors[0];
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
return callback(new BoxError(BoxError.ACCESS_DENIED, message));
}
@@ -290,7 +284,7 @@ function verifyDnsConfig(domainObject, callback) {
if (dnsConfig.tokenType !== 'GlobalApiKey' && dnsConfig.tokenType !== 'ApiToken') return callback(new BoxError(BoxError.BAD_FIELD, 'tokenType is required', { field: 'tokenType' }));
if (dnsConfig.tokenType === 'GlobalApiKey') {
if (typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
if ('email' in dnsConfig && typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
}
const ip = '127.0.0.1';
+2 -3
View File
@@ -13,7 +13,6 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -29,12 +28,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.token = constants.SECRET_PLACEHOLDER;
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getInternal(dnsConfig, zoneName, name, type, callback) {
+2 -3
View File
@@ -12,7 +12,6 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/gandi'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -27,12 +26,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.token = constants.SECRET_PLACEHOLDER;
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function upsert(domainObject, location, type, values, callback) {
+2 -3
View File
@@ -12,7 +12,6 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/gcdns'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -22,12 +21,12 @@ var assert = require('assert'),
_ = require('underscore');
function removePrivateFields(domainObject) {
domainObject.config.credentials.private_key = constants.SECRET_PLACEHOLDER;
domainObject.config.credentials.private_key = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.credentials.private_key === constants.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
if (newConfig.credentials.private_key === domains.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
}
function getDnsCredentials(dnsConfig) {
+2 -3
View File
@@ -12,7 +12,6 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/godaddy'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -33,12 +32,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.apiSecret = constants.SECRET_PLACEHOLDER;
domainObject.config.apiSecret = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.apiSecret === constants.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
if (newConfig.apiSecret === domains.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
}
function upsert(domainObject, location, type, values, callback) {
+2 -2
View File
@@ -21,13 +21,13 @@ var assert = require('assert'),
util = require('util');
function removePrivateFields(domainObject) {
// in-place removal of tokens and api keys with constants.SECRET_PLACEHOLDER
// in-place removal of tokens and api keys with domains.SECRET_PLACEHOLDER
return domainObject;
}
// eslint-disable-next-line no-unused-vars
function injectPrivateFields(newConfig, currentConfig) {
// in-place injection of tokens and api keys which came in with constants.SECRET_PLACEHOLDER
// in-place injection of tokens and api keys which came in with domains.SECRET_PLACEHOLDER
}
function upsert(domainObject, location, type, values, callback) {
+2 -3
View File
@@ -12,7 +12,6 @@ exports = module.exports = {
let async = require('async'),
assert = require('assert'),
constants = require('../constants.js'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/linode'),
dns = require('../native-dns.js'),
@@ -28,12 +27,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.token = constants.SECRET_PLACEHOLDER;
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getZoneId(dnsConfig, zoneName, callback) {
+2 -3
View File
@@ -12,7 +12,6 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/namecheap'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -26,12 +25,12 @@ var assert = require('assert'),
const ENDPOINT = 'https://api.namecheap.com/xml.response';
function removePrivateFields(domainObject) {
domainObject.config.token = constants.SECRET_PLACEHOLDER;
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getQuery(dnsConfig, callback) {
+2 -11
View File
@@ -12,7 +12,6 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/namecom'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -28,12 +27,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.token = constants.SECRET_PLACEHOLDER;
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function addRecord(dnsConfig, zoneName, name, type, values, callback) {
@@ -55,10 +54,6 @@ function addRecord(dnsConfig, zoneName, name, type, values, callback) {
if (type === 'MX') {
data.priority = parseInt(values[0].split(' ')[0], 10);
data.answer = values[0].split(' ')[1];
} else if (type === 'TXT') {
// we have to strip the quoting for some odd reason for name.com! If you change that also change updateRecord
let tmp = values[0];
data.answer = tmp.indexOf('"') === 0 && tmp.lastIndexOf('"') === tmp.length-1 ? tmp.slice(1, tmp.length-1) : tmp;
} else {
data.answer = values[0];
}
@@ -96,10 +91,6 @@ function updateRecord(dnsConfig, zoneName, recordId, name, type, values, callbac
if (type === 'MX') {
data.priority = parseInt(values[0].split(' ')[0], 10);
data.answer = values[0].split(' ')[1];
} else if (type === 'TXT') {
// we have to strip the quoting for some odd reason for name.com! If you change that also change addRecord
let tmp = values[0];
data.answer = tmp.indexOf('"') === 0 && tmp.lastIndexOf('"') === tmp.length-1 ? tmp.slice(1, tmp.length-1) : tmp;
} else {
data.answer = values[0];
}
+2 -3
View File
@@ -13,7 +13,6 @@ exports = module.exports = {
var assert = require('assert'),
AWS = require('aws-sdk'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/route53'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -22,12 +21,12 @@ var assert = require('assert'),
_ = require('underscore');
function removePrivateFields(domainObject) {
domainObject.config.secretAccessKey = constants.SECRET_PLACEHOLDER;
domainObject.config.secretAccessKey = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.secretAccessKey === constants.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
if (newConfig.secretAccessKey === domains.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
}
function getDnsCredentials(dnsConfig) {
+39 -26
View File
@@ -6,6 +6,8 @@ exports = module.exports = {
injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
ping: ping,
info: info,
@@ -53,6 +55,12 @@ const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH });
function debugApp(app) {
assert(typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function testRegistryConfig(auth, callback) {
assert.strictEqual(typeof auth, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -65,13 +73,13 @@ function testRegistryConfig(auth, callback) {
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.password === constants.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
if (newConfig.password === exports.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
}
function removePrivateFields(registryConfig) {
assert.strictEqual(typeof registryConfig, 'object');
if (registryConfig.password) registryConfig.password = constants.SECRET_PLACEHOLDER;
if (registryConfig.password) registryConfig.password = exports.SECRET_PLACEHOLDER;
return registryConfig;
}
@@ -180,19 +188,6 @@ function downloadImage(manifest, callback) {
}, callback);
}
function getBindsSync(app) {
assert.strictEqual(typeof app, 'object');
let binds = [];
for (let name of Object.keys(app.binds)) {
const bind = app.binds[name];
binds.push(`${bind.hostPath}:/media/${name}:${bind.readOnly ? 'ro' : 'rw'}`);
}
return binds;
}
function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
@@ -282,7 +277,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
},
HostConfig: {
Mounts: addons.getMountsSync(app, app.manifest.addons),
Binds: getBindsSync(app), // ideally, we have to use 'Mounts' but we have to create volumes then
LogConfig: {
Type: 'syslog',
Config: {
@@ -305,8 +299,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
NetworkMode: 'cloudron', // user defined bridge network
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapDrop: [ 'NET_RAW' ] // https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
},
NetworkingConfig: {
EndpointsConfig: {
@@ -320,12 +313,14 @@ function createSubcontainer(app, name, cmd, options, callback) {
var capabilities = manifest.capabilities || [];
if (capabilities.includes('net_admin')) {
containerOptions.HostConfig.CapAdd = [
'NET_ADMIN', 'NET_RAW'
'NET_ADMIN'
];
}
containerOptions = _.extend(containerOptions, options);
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
gConnection.createContainer(containerOptions, function (error, container) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
@@ -343,6 +338,7 @@ function startContainer(containerId, callback) {
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
debug('Starting container %s', containerId);
container.start(function (error) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
@@ -358,6 +354,7 @@ function restartContainer(containerId, callback) {
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
debug('Restarting container %s', containerId);
container.restart(function (error) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
@@ -378,6 +375,7 @@ function stopContainer(containerId, callback) {
}
var container = gConnection.getContainer(containerId);
debug('Stopping container %s', containerId);
var options = {
t: 10 // wait for 10 seconds before killing it
@@ -386,9 +384,13 @@ function stopContainer(containerId, callback) {
container.stop(options, function (error) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error stopping container:' + error.message));
container.wait(function (error/*, data */) {
debug('Waiting for container ' + containerId);
container.wait(function (error, data) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error waiting on container:' + error.message));
debug('Container %s stopped with status code [%s]', containerId, data ? String(data.StatusCode) : '');
return callback(null);
});
});
@@ -398,6 +400,8 @@ function deleteContainer(containerId, callback) {
assert(!containerId || typeof containerId === 'string');
assert.strictEqual(typeof callback, 'function');
debug('deleting container %s', containerId);
if (containerId === null) return callback(null);
var container = gConnection.getContainer(containerId);
@@ -424,6 +428,8 @@ function deleteContainers(appId, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('deleting containers of %s', appId);
let labels = [ 'appId=' + appId ];
if (options.managedOnly) labels.push('isCloudronManaged=true');
@@ -440,6 +446,8 @@ function stopContainers(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('Stopping containers of %s', appId);
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
@@ -565,10 +573,10 @@ function memoryUsage(containerId, callback) {
});
}
function createVolume(name, volumeDataDir, labels, callback) {
function createVolume(app, name, volumeDataDir, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof volumeDataDir, 'string');
assert.strictEqual(typeof labels, 'object');
assert.strictEqual(typeof callback, 'function');
const volumeOptions = {
@@ -579,7 +587,10 @@ function createVolume(name, volumeDataDir, labels, callback) {
device: volumeDataDir,
o: 'bind'
},
Labels: labels
Labels: {
'fqdn': app.fqdn,
'appId': app.id
},
};
// requires sudo because the path can be outside appsdata
@@ -594,7 +605,8 @@ function createVolume(name, volumeDataDir, labels, callback) {
});
}
function clearVolume(name, options, callback) {
function clearVolume(app, name, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -614,13 +626,14 @@ function clearVolume(name, options, callback) {
}
// this only removes the volume and not the data
function removeVolume(name, callback) {
function removeVolume(app, name, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
let volume = gConnection.getVolume(name);
volume.remove(function (error) {
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume: ${error.message}`));
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume of ${app.id} ${error.message}`));
callback();
});
+16 -15
View File
@@ -23,6 +23,11 @@ var apps = require('./apps.js'),
var gHttpServer = null;
function authorizeApp(req, res, next) {
// TODO add here some authorization
// - block apps not using the docker addon
// - block calls regarding platform containers
// - only allow managing and inspection of containers belonging to the app
// make the tests pass for now
if (constants.TEST) {
req.app = { id: 'testappid' };
@@ -59,8 +64,6 @@ function attachDockerRequest(req, res, next) {
dockerResponse.pipe(res, { end: true });
});
req.dockerRequest.on('error', () => {}); // abort() throws
next();
}
@@ -71,21 +74,22 @@ function containersCreate(req, res, next) {
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id, isCloudronManaged: String(false) })); // overwrite the app id to track containers of an app
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data');
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
dockerDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'docker');
debug('Original bind mounts:', req.body.HostConfig.Binds);
debug('Original volume binds:', req.body.HostConfig.Binds);
let binds = [];
for (let bind of (req.body.HostConfig.Binds || [])) {
if (!bind.startsWith('/app/data/')) {
req.dockerRequest.abort();
return next(new HttpError(400, 'Binds must be under /app/data/'));
}
binds.push(bind.replace(new RegExp('^/app/data/'), appDataDir + '/'));
if (bind.startsWith(appDataDir)) binds.push(bind); // eclipse will inspect docker to find out the host folders and pass that to child containers
else if (bind.startsWith('/app/data')) binds.push(bind.replace(new RegExp('^/app/data'), appDataDir));
else binds.push(`${dockerDataDir}/${bind}`);
}
debug('Rewritten bind mounts:', binds);
// cleanup the paths from potential double slashes
binds = binds.map(function (bind) { return bind.replace(/\/+/g, '/'); });
debug('Rewritten volume binds:', binds);
safe.set(req.body, 'HostConfig.Binds', binds);
let plainBody = JSON.stringify(req.body);
@@ -113,9 +117,6 @@ function start(callback) {
assert(gHttpServer === null, 'Already started');
let json = middleware.json({ strict: true });
// we protect container create as the app/admin can otherwise mount random paths (like the ghost file)
// protected other paths is done by preventing install/exec access of apps using docker addon
let router = new express.Router();
router.post('/:version/containers/create', containersCreate);
@@ -136,7 +137,7 @@ function start(callback) {
.use(middleware.lastMile());
gHttpServer = http.createServer(proxyServer);
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '172.18.0.1', callback);
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '0.0.0.0', callback);
// Overwrite the default 2min request timeout. This is required for large builds for example
gHttpServer.setTimeout(60 * 60 * 1000);
+9 -20
View File
@@ -51,21 +51,16 @@ function getAll(callback) {
});
}
function add(name, data, callback) {
function add(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof data.zoneName, 'string');
assert.strictEqual(typeof data.provider, 'string');
assert.strictEqual(typeof data.config, 'object');
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof domain, 'object');
assert.strictEqual(typeof domain.zoneName, 'string');
assert.strictEqual(typeof domain.provider, 'string');
assert.strictEqual(typeof domain.config, 'object');
assert.strictEqual(typeof domain.tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
let queries = [
{ query: 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', args: [ name, data.zoneName, data.provider, JSON.stringify(data.config), JSON.stringify(data.tlsConfig) ] },
{ query: 'INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', args: [ name, data.dkimSelector || 'cloudron' ] },
];
database.transaction(queries, function (error) {
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
@@ -105,12 +100,7 @@ function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
let queries = [
{ query: 'DELETE FROM mail WHERE domain = ?', args: [ domain ] },
{ query: 'DELETE FROM domains WHERE domain = ?', args: [ domain ] },
];
database.transaction(queries, function (error, results) {
database.query('DELETE FROM domains WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') {
if (error.message.indexOf('apps_mailDomain_constraint') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by an app or the mailbox of an app. Check the domains of apps and the Email section of each app.'));
if (error.message.indexOf('subdomains') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more app(s).'));
@@ -118,9 +108,8 @@ function del(domain, callback) {
return callback(new BoxError(BoxError.CONFLICT, error.message));
}
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results[1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
callback(null);
});
+5 -12
View File
@@ -28,7 +28,9 @@ module.exports = exports = {
checkDnsRecords: checkDnsRecords,
prepareDashboardDomain: prepareDashboardDomain
prepareDashboardDomain: prepareDashboardDomain,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
};
var assert = require('assert'),
@@ -38,7 +40,6 @@ var assert = require('assert'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
@@ -47,8 +48,6 @@ var assert = require('assert'),
util = require('util'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
assert.strictEqual(typeof provider, 'string');
@@ -172,7 +171,7 @@ function add(domain, data, auditSource, callback) {
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
let { zoneName, provider, config, fallbackCertificate, tlsConfig, dkimSelector } = data;
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
if (!tld.isValid(domain)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
if (domain.endsWith('.')) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
@@ -195,12 +194,10 @@ function add(domain, data, auditSource, callback) {
let error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (!dkimSelector) dkimSelector = 'cloudron-' + settings.adminDomain().replace(/\./g, '');
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
domaindb.add(domain, { zoneName, provider, config: sanitizedConfig, tlsConfig, dkimSelector }, function (error) {
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
if (error) return callback(error);
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
@@ -208,8 +205,6 @@ function add(domain, data, auditSource, callback) {
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
mail.onDomainAdded(domain, NOOP_CALLBACK);
callback();
});
});
@@ -319,8 +314,6 @@ function del(domain, auditSource, callback) {
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
mail.onDomainRemoved(domain, NOOP_CALLBACK);
return callback(null);
});
}
+8 -9
View File
@@ -9,19 +9,18 @@ exports = module.exports = {
'version': '48.17.0',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:2.0.0@sha256:f9fea80513aa7c92fe2e7bf3978b54c8ac5222f47a9a32a7f8833edf0eb5a4f4' }
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
],
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
'images': {
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.1.0@sha256:e1dd22aa6eef5beb7339834b200a8bb787ffc2264ce11139857a054108fefb4f' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.0@sha256:0cc39004b80eb5ee570b9fd4f38859410a49692e93e0f0d8b104b0b524d7030a' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.1.1@sha256:6ea1bcf9b655d628230acda01d46cf21883b112111d89583c94138646895c14c' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.2.0@sha256:205486ff0f6bf6854610572df401cf3651bc62baf28fd26e9c5632497f10c2cb' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.2.0@sha256:cfdcc1a54cf29818cac99eacedc2ecf04e44995be3d06beea11dcaa09d90ed8d' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.9.3@sha256:2c062e8883cadae4b4c22553f9db141fc441d6c627aa85e2a5ad71e3fcbf2b42' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.3.0@sha256:b7bc1ca4f4d0603a01369a689129aa273a938ce195fe43d00d42f4f2d5212f50' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:1.1.0@sha256:0c1fe4dd6121900624dcb383251ecb0084c3810e095064933de671409d8d6d7b' }
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.1.0@sha256:eee0dfd3829d563f2063084bc0d7c8802c4bdd6e233159c6226a17ff7a9a3503' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.1.0@sha256:f2cda21bd15c21bbf44432df412525369ef831a2d53860b5c5b1675e6f384de2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.6.5@sha256:d17cc0a3d2b6431cb683109abf40fffb91199e2af1d6d99f81d8ec3a1e1bb442' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.2.0@sha256:fc9ca69d16e6ebdbd98ed53143d4a0d2212eef60cb638dc71219234e6f427a2c' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
}
};
+11 -11
View File
@@ -154,6 +154,7 @@ function userSearch(req, res, next) {
givenName: firstName,
username: user.username,
samaccountname: user.username, // to support ActiveDirectory clients
isadmin: users.compareRoles(user.role, users.ROLE_ADMIN) >= 0,
memberof: groups
}
};
@@ -391,7 +392,7 @@ function mailAliasSearch(req, res, next) {
objectclass: ['nisMailAlias'],
objectcategory: 'nisMailAlias',
cn: `${alias.name}@${alias.domain}`,
rfc822MailMember: `${alias.aliasName}@${alias.aliasDomain}`
rfc822MailMember: `${alias.aliasTarget}@${alias.domain}`
}
};
@@ -417,7 +418,7 @@ function mailingListSearch(req, res, next) {
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const name = parts[0], domain = parts[1];
mail.resolveList(parts[0], parts[1], function (error, resolvedMembers, list) {
mail.resolveList(parts[0], parts[1], function (error, resolvedMembers) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
@@ -430,7 +431,6 @@ function mailingListSearch(req, res, next) {
objectcategory: 'mailGroup',
cn: `${name}@${domain}`, // fully qualified
mail: `${name}@${domain}`,
membersOnly: list.membersOnly, // ldapjs only supports strings and string array. so this is not a bool!
mgrpRFC822MailMember: resolvedMembers // fully qualified
}
};
@@ -534,16 +534,13 @@ function authenticateSftp(req, res, next) {
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
apps.getByFqdn(parts[1], function (error, app) {
// actual user bind
users.verifyWithUsername(parts[0], req.credentials, users.AP_SFTP, function (error) {
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
users.verifyWithUsername(parts[0], req.credentials, app.id, function (error) {
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
debug('sftp auth: success');
debug('sftp auth: success');
res.end();
});
res.end();
});
}
@@ -618,7 +615,10 @@ function authenticateMailAddon(req, res, next) {
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) {
if (error && error.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
if (appId) return res.end();
if (appId) { // matched app password
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
return res.end();
}
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
-2
View File
@@ -8,7 +8,6 @@
maxsize 1M
missingok
delaycompress
# this truncates the original log file and not the rotated one
copytruncate
}
@@ -19,7 +18,6 @@
missingok
# we never compress so we can simply tail the files
nocompress
# this truncates the original log file and not the rotated one
copytruncate
}
+59 -40
View File
@@ -7,11 +7,10 @@ exports = module.exports = {
getDomains: getDomains,
getDomain: getDomain,
addDomain: addDomain,
removeDomain: removeDomain,
clearDomains: clearDomains,
onDomainAdded: onDomainAdded,
onDomainRemoved: onDomainRemoved,
removePrivateFields: removePrivateFields,
setDnsRecords: setDnsRecords,
@@ -38,6 +37,7 @@ exports = module.exports = {
updateMailboxOwner: updateMailboxOwner,
removeMailbox: removeMailbox,
listAliases: listAliases,
getAliases: getAliases,
setAliases: setAliases,
@@ -207,8 +207,7 @@ function checkDkim(mailDomain, callback) {
if (txtRecords.length !== 0) {
dkim.value = txtRecords[0].join('');
const actual = txtToDict(dkim.value);
dkim.status = actual.p === dkimKey;
dkim.status = (dkim.value === dkim.expected);
}
callback(null, dkim);
@@ -269,7 +268,7 @@ function checkMx(domain, mailFqdn, callback) {
if (error) return callback(error, mx);
if (mxRecords.length === 0) return callback(null, mx);
mx.status = mxRecords.some(mx => mx.exchange === mailFqdn); // this lets use change priority and/or setup backup MX
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
if (mx.status) return callback(null, mx); // MX record is "my."
@@ -313,8 +312,9 @@ function checkDmarc(domain, callback) {
if (txtRecords.length !== 0) {
dmarc.value = txtRecords[0].join('');
const actual = txtToDict(dmarc.value);
dmarc.status = actual.v === 'DMARC1'; // see box#666
// allow extra fields in dmarc like rua
const actual = txtToDict(dmarc.value), expected = txtToDict(dmarc.expected);
dmarc.status = Object.keys(expected).every(k => expected[k] === actual[k]);
}
callback(null, dmarc);
@@ -798,7 +798,6 @@ function ensureDkimKeySync(mailDomain) {
return new BoxError(BoxError.FS_ERROR, safe.error);
}
// https://www.unlocktheinbox.com/dkim-key-length-statistics/ and https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-authentication-dkim-easy.html for key size
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
@@ -906,21 +905,37 @@ function onMailFqdnChanged(callback) {
});
}
function onDomainAdded(domain, callback) {
function addDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
async.series([
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], callback);
const dkimSelector = domain === settings.adminDomain() ? 'cloudron' : ('cloudron-' + settings.adminDomain().replace(/\./g, ''));
maildb.add(domain, { dkimSelector }, function (error) {
if (error) return callback(error);
async.series([
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], NOOP_CALLBACK); // do these asynchronously
callback();
});
}
function onDomainRemoved(domain, callback) {
function removeDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
restartMail(callback);
if (domain === settings.adminDomain()) return callback(new BoxError(BoxError.CONFLICT));
maildb.del(domain, function (error) {
if (error) return callback(error);
restartMail(NOOP_CALLBACK);
callback();
});
}
function clearDomains(callback) {
@@ -1126,6 +1141,19 @@ function removeMailbox(name, domain, auditSource, callback) {
});
}
function listAliases(domain, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listAliases(domain, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
}
function getAliases(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
@@ -1149,15 +1177,12 @@ function setAliases(name, domain, aliases, callback) {
assert.strictEqual(typeof callback, 'function');
for (var i = 0; i < aliases.length; i++) {
let name = aliases[i].name.toLowerCase();
let domain = aliases[i].domain.toLowerCase();
aliases[i] = aliases[i].toLowerCase();
let error = validateName(name);
var error = validateName(aliases[i]);
if (error) return callback(error);
if (!validator.isEmail(`${name}@${domain}`)) return callback(new BoxError(BoxError.BAD_FIELD, `Invalid email: ${name}@${domain}`));
aliases[i] = { name, domain };
}
mailboxdb.setAliasesForName(name, domain, aliases, function (error) {
if (error) return callback(error);
@@ -1188,11 +1213,10 @@ function getList(name, domain, callback) {
});
}
function addList(name, domain, members, membersOnly, auditSource, callback) {
function addList(name, domain, members, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1205,20 +1229,19 @@ function addList(name, domain, members, membersOnly, auditSource, callback) {
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
}
mailboxdb.addList(name, domain, members, membersOnly, function (error) {
mailboxdb.addList(name, domain, members, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members, membersOnly });
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members });
callback();
});
}
function updateList(name, domain, members, membersOnly, auditSource, callback) {
function updateList(name, domain, members, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1234,10 +1257,10 @@ function updateList(name, domain, members, membersOnly, auditSource, callback) {
getList(name, domain, function (error, result) {
if (error) return callback(error);
mailboxdb.updateList(name, domain, members, membersOnly, function (error) {
mailboxdb.updateList(name, domain, members, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members, membersOnly });
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members });
callback(null);
});
@@ -1259,7 +1282,6 @@ function removeList(name, domain, auditSource, callback) {
});
}
// resolves the members of a list. i.e the lists and aliases
function resolveList(listName, listDomain, callback) {
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof listDomain, 'string');
@@ -1290,21 +1312,18 @@ function resolveList(listName, listDomain, callback) {
visited.push(member);
mailboxdb.get(memberName, memberDomain, function (error, entry) {
if (error && error.reason == BoxError.NOT_FOUND) { result.push(member); return iteratorCallback(); } // let it bounce
if (error && error.reason == BoxError.NOT_FOUND) { result.push(member); return iteratorCallback(); }
if (error) return iteratorCallback(error);
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.aliasTarget}`);
} else { // resolve list members
toResolve = toResolve.concat(entry.members);
}
if (entry.type === mailboxdb.TYPE_MAILBOX) { result.push(member); return iteratorCallback(); }
// no need to resolve alias because we only allow one level and within same domain
if (entry.type === mailboxdb.TYPE_ALIAS) { result.push(`${entry.aliasTarget}@${entry.domain}`); return iteratorCallback(); }
toResolve = toResolve.concat(entry.members);
iteratorCallback();
});
}, function (error) {
callback(error, result, list);
callback(error, result);
});
});
});
+30 -16
View File
@@ -8,6 +8,7 @@ exports = module.exports = {
updateList: updateList,
del: del,
listAliases: listAliases,
listMailboxes: listMailboxes,
getLists: getLists,
@@ -40,14 +41,12 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain' ].join(',');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
function postProcess(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ];
delete data.membersJson;
data.membersOnly = !!data.membersOnly;
return data;
}
@@ -79,15 +78,14 @@ function updateMailboxOwner(name, domain, ownerId, callback) {
});
}
function addList(name, domain, members, membersOnly, callback) {
function addList(name, domain, members, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson, membersOnly) VALUES (?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members), membersOnly ], function (error) {
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson) VALUES (?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members) ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
@@ -95,15 +93,14 @@ function addList(name, domain, members, membersOnly, callback) {
});
}
function updateList(name, domain, members, membersOnly, callback) {
function updateList(name, domain, members, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE mailboxes SET membersJson = ?, membersOnly = ? WHERE name = ? AND domain = ?',
[ JSON.stringify(members), membersOnly, name, domain ], function (error, result) {
database.query('UPDATE mailboxes SET membersJson = ? WHERE name = ? AND domain = ?',
[ JSON.stringify(members), name, domain ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
@@ -126,7 +123,7 @@ function del(name, domain, callback) {
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mailboxes WHERE ((name=? AND domain=?) OR (aliasName = ? AND aliasDomain=?))', [ name, domain, name, domain ], function (error, result) {
database.query('DELETE FROM mailboxes WHERE (name=? OR aliasTarget = ?) AND domain = ?', [ name, name, domain ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
@@ -288,10 +285,10 @@ function setAliasesForName(name, domain, aliases, callback) {
var queries = [];
// clear existing aliases
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId) VALUES (?, ?, ?, ?, ?, ?)',
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId ] });
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId) VALUES (?, ?, ?, ?, ?)',
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId ] });
});
database.transaction(queries, function (error) {
@@ -314,10 +311,27 @@ function getAliasesForName(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name, domain FROM mailboxes WHERE type = ? AND aliasName = ? AND aliasDomain = ? ORDER BY name',
database.query('SELECT name FROM mailboxes WHERE type = ? AND aliasTarget = ? AND domain = ? ORDER BY name',
[ exports.TYPE_ALIAS, name, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results = results.map(function (r) { return r.name; });
callback(null, results);
});
}
function listAliases(domain, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE domain = ? AND type = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ domain, exports.TYPE_ALIAS ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
+30
View File
@@ -1,6 +1,8 @@
'use strict';
exports = module.exports = {
add: add,
del: del,
get: get,
list: list,
update: update,
@@ -32,6 +34,20 @@ function postProcess(data) {
return data;
}
function add(domain, data, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', [ domain, data.dkimSelector || 'cloudron' ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mail domain already exists'));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND), 'no such domain');
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -42,6 +58,20 @@ function clear(callback) {
});
}
function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mail WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new BoxError(BoxError.CONFLICT));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mail domain not found'));
callback(null);
});
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
+10 -22
View File
@@ -58,16 +58,17 @@ server {
ssl_certificate <%= certFilePath %>;
ssl_certificate_key <%= keyFilePath %>;
ssl_session_timeout 5m;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
ssl_session_cache shared:SSL:50m;
# https://bettercrypto.org/static/applied-crypto-hardening.pdf
# https://mozilla.github.io/server-side-tls/ssl-config-generator/
# https://cipherli.st/
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
# https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices#25-use-forward-secrecy
# ciphers according to https://ssl-config.mozilla.org/#server=nginx&version=1.14.0&config=intermediate&openssl=1.1.1&guideline=5.4
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256;
ssl_prefer_server_ciphers off;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
# ciphers according to https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.3&openssl=1.0.2g&hsts=yes&profile=modern
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
@@ -135,21 +136,8 @@ server {
# internal means this is for internal routing and cannot be accessed as URL from browser
internal;
}
location @wellknown-upstream {
<% if ( endpoint === 'admin' ) { %>
proxy_pass http://127.0.0.1:3000;
<% } else if ( endpoint === 'app' ) { %>
proxy_pass http://127.0.0.1:<%= port %>;
<% } else if ( endpoint === 'redirect' ) { %>
return 302 https://<%= redirectTo %>$request_uri;
<% } %>
}
# user defined .well-known resources
location ~ ^/.well-known/(.*)$ {
root /home/yellowtent/boxdata/well-known/$host;
try_files /$1 @wellknown-upstream;
location /appstatus.html {
internal;
}
location / {
+1 -2
View File
@@ -22,7 +22,7 @@ exports = module.exports = {
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
APPS_DATA_DIR: path.join(baseDir(), 'appsdata'),
BOX_DATA_DIR: path.join(baseDir(), 'boxdata'), // box data dir is part of box backup
BOX_DATA_DIR: path.join(baseDir(), 'boxdata'),
ACME_CHALLENGES_DIR: path.join(baseDir(), 'platformdata/acme'),
ADDON_CONFIG_DIR: path.join(baseDir(), 'platformdata/addons'),
@@ -46,7 +46,6 @@ exports = module.exports = {
APP_CERTS_DIR: path.join(baseDir(), 'boxdata/certs'),
CLOUDRON_AVATAR_FILE: path.join(baseDir(), 'boxdata/avatar.png'),
UPDATE_CHECKER_FILE: path.join(baseDir(), 'boxdata/updatechecker.json'),
ADDON_TURN_SECRET_FILE: path.join(baseDir(), 'boxdata/addon-turn-secret'),
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
+7 -18
View File
@@ -133,10 +133,11 @@ function pruneInfraImages(callback) {
function stopContainers(existingInfra, callback) {
// always stop addons to restart them on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
// TODO: only nuke containers with isCloudronManaged=true
debug('stopping all containers for infra upgrade');
async.series([
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'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')
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker stop'),
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker rm -f')
], callback);
} else {
assert(typeof infra.images, 'object');
@@ -149,8 +150,8 @@ function stopContainers(existingInfra, callback) {
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
// ignore error if container not found (and fail later) so that this code works across restarts
async.series([
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter '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`)
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'network=cloudron' | xargs --no-run-if-empty docker stop || true`),
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'network=cloudron' | xargs --no-run-if-empty docker rm -f || true`)
], callback);
}
}
@@ -164,19 +165,7 @@ function startApps(existingInfra, callback) {
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
apps.configureInstalledApps(callback);
} else {
let changedAddons = [];
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) changedAddons.push('mysql');
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) changedAddons.push('postgresql');
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) changedAddons.push('mongodb');
if (infra.images.redis.tag !== existingInfra.images.redis.tag) changedAddons.push('redis');
if (changedAddons.length) {
// restart apps if docker image changes since the IP changes and any "persistent" connections fail
debug(`startApps: changedAddons: ${JSON.stringify(changedAddons)}`);
apps.restartAppsUsingAddons(changedAddons, callback);
} else {
debug('startApps: apps are already uptodate');
callback();
}
debug('startApps: apps are already uptodate');
callback();
}
}
+3 -10
View File
@@ -121,8 +121,7 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
provider: dnsConfig.provider,
config: dnsConfig.config,
fallbackCertificate: dnsConfig.fallbackCertificate || null,
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' },
dkimSelector: 'cloudron'
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
};
domains.add(domain, data, auditSource, function (error) {
@@ -138,6 +137,7 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
settings.setSysinfoConfig.bind(null, sysinfoConfig),
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
mail.addDomain.bind(null, domain), // this relies on settings.mailFqdn() and settings.adminDomain()
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
@@ -206,16 +206,9 @@ function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, ca
if (error) return done(error);
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.'));
backups.testProviderConfig(backupConfig, function (error) {
backups.testConfig(backupConfig, function (error) {
if (error) return done(error);
if ('password' in backupConfig) {
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password);
delete backupConfig.password;
} else {
backupConfig.encryption = null;
}
sysinfo.testConfig(sysinfoConfig, function (error) {
if (error) return done(error);
+8 -8
View File
@@ -79,7 +79,7 @@ function getCertApi(domainObject, callback) {
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
users.getOwner(function (error, owner) {
options.email = error ? 'webmaster@cloudron.io' : owner.email; // can error if not activated yet
options.email = error ? 'support@cloudron.io' : owner.email; // can error if not activated yet
callback(null, api, options);
});
@@ -146,19 +146,19 @@ function validateCertificate(location, domainObject, certificate) {
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
const fqdn = domains.fqdn(location, domainObject);
let result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
if (result === null) return new BoxError(BoxError.BAD_FIELD, 'Unable to get certificate subject:' + safe.error.message, { field: 'cert' });
if (result.indexOf('does match certificate') === -1) return new BoxError(BoxError.BAD_FIELD, `Certificate is not valid for this domain. Expecting ${fqdn}`, { field: 'cert' });
// check if public key in the cert and private key matches. pkey below works for RSA and ECDSA keys
const pubKeyFromCert = safe.child_process.execSync('openssl x509 -noout -pubkey', { encoding: 'utf8', input: cert });
if (pubKeyFromCert === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get public key from cert: ${safe.error.message}`, { field: 'cert' });
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
if (certModulus === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get cert modulus: ${safe.error.message}`, { field: 'cert' });
const pubKeyFromKey = safe.child_process.execSync('openssl pkey -pubout', { encoding: 'utf8', input: key });
if (pubKeyFromKey === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get public key from private key: ${safe.error.message}`, { field: 'cert' });
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (keyModulus === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get key modulus: ${safe.error.message}`, { field: 'cert' });
if (pubKeyFromCert !== pubKeyFromKey) return new BoxError(BoxError.BAD_FIELD, 'Public key does not match the certificate.', { field: 'cert' });
if (certModulus !== keyModulus) return new BoxError(BoxError.BAD_FIELD, 'Key does not match the certificate.', { field: 'cert' });
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
+98 -154
View File
@@ -4,16 +4,16 @@ exports = module.exports = {
getApp: getApp,
getApps: getApps,
getAppIcon: getAppIcon,
install: install,
uninstall: uninstall,
restore: restore,
installApp: installApp,
uninstallApp: uninstallApp,
restoreApp: restoreApp,
importApp: importApp,
backup: backup,
update: update,
backupApp: backupApp,
updateApp: updateApp,
getLogs: getLogs,
getLogStream: getLogStream,
listBackups: listBackups,
repair: repair,
repairApp: repairApp,
setAccessRestriction: setAccessRestriction,
setLabel: setLabel,
@@ -30,20 +30,17 @@ exports = module.exports = {
setMailbox: setMailbox,
setLocation: setLocation,
setDataDir: setDataDir,
setBinds: setBinds,
stop: stop,
start: start,
restart: restart,
stopApp: stopApp,
startApp: startApp,
restartApp: restartApp,
exec: exec,
execWebSocket: execWebSocket,
clone: clone,
cloneApp: cloneApp,
uploadFile: uploadFile,
downloadFile: downloadFile,
load: load
downloadFile: downloadFile
};
var apps = require('../apps.js'),
@@ -54,28 +51,19 @@ var apps = require('../apps.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
users = require('../users.js'),
util = require('util'),
WebSocket = require('ws');
function load(req, res, next) {
function getApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.get(req.params.id, function (error, result) {
apps.get(req.params.id, function (error, app) {
if (error) return next(BoxError.toHttpError(error));
req.resource = result;
next();
next(new HttpSuccess(200, apps.removeInternalFields(app)));
});
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
next(new HttpSuccess(200, apps.removeInternalFields(req.resource)));
}
function getApps(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
@@ -89,19 +77,19 @@ function getApps(req, res, next) {
}
function getAppIcon(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
apps.getIconPath(req.resource, { original: req.query.original }, function (error, iconPath) {
apps.getIconPath(req.params.id, { original: req.query.original }, function (error, iconPath) {
if (error) return next(BoxError.toHttpError(error));
res.sendFile(iconPath);
});
}
function install(req, res, next) {
function installApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
const data = req.body;
var data = req.body;
// atleast one
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
@@ -145,28 +133,20 @@ function install(req, res, next) {
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
apps.install(data, req.user, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to install app with docker addon'));
data.appStoreId = appStoreId;
data.manifest = manifest;
apps.install(data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
});
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
});
}
function setAccessRestriction(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
apps.setAccessRestriction(req.resource, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
apps.setAccessRestriction(req.params.id, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -175,11 +155,11 @@ function setAccessRestriction(req, res, next) {
function setLabel(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.body.label !== 'string') return next(new HttpError(400, 'label must be a string'));
apps.setLabel(req.resource, req.body.label, auditSource.fromRequest(req), function (error) {
apps.setLabel(req.params.id, req.body.label, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -188,12 +168,12 @@ function setLabel(req, res, next) {
function setTags(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (!Array.isArray(req.body.tags)) return next(new HttpError(400, 'tags must be an array'));
if (req.body.tags.some((t) => typeof t !== 'string')) return next(new HttpError(400, 'tags array must contain strings'));
apps.setTags(req.resource, req.body.tags, auditSource.fromRequest(req), function (error) {
apps.setTags(req.params.id, req.body.tags, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -202,11 +182,11 @@ function setTags(req, res, next) {
function setIcon(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.icon !== null && typeof req.body.icon !== 'string') return next(new HttpError(400, 'icon is null or a base-64 image string'));
apps.setIcon(req.resource, req.body.icon, auditSource.fromRequest(req), function (error) {
apps.setIcon(req.params.id, req.body.icon, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -215,11 +195,11 @@ function setIcon(req, res, next) {
function setMemoryLimit(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
apps.setMemoryLimit(req.resource, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
apps.setMemoryLimit(req.params.id, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -228,11 +208,11 @@ function setMemoryLimit(req, res, next) {
function setCpuShares(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.body.cpuShares !== 'number') return next(new HttpError(400, 'cpuShares is not a number'));
apps.setCpuShares(req.resource, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) {
apps.setCpuShares(req.params.id, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -241,11 +221,11 @@ function setCpuShares(req, res, next) {
function setAutomaticBackup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
apps.setAutomaticBackup(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) {
apps.setAutomaticBackup(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -254,11 +234,11 @@ function setAutomaticBackup(req, res, next) {
function setAutomaticUpdate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
apps.setAutomaticUpdate(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) {
apps.setAutomaticUpdate(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -267,13 +247,13 @@ function setAutomaticUpdate(req, res, next) {
function setReverseProxyConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.robotsTxt !== null && typeof req.body.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt is not a string'));
if (req.body.csp !== null && typeof req.body.csp !== 'string') return next(new HttpError(400, 'csp is not a string'));
apps.setReverseProxyConfig(req.resource, req.body, auditSource.fromRequest(req), function (error) {
apps.setReverseProxyConfig(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -282,14 +262,14 @@ function setReverseProxyConfig(req, res, next) {
function setCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.key !== null && typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (req.body.cert !== null && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (req.body.cert && !req.body.key) return next(new HttpError(400, 'key must be provided'));
if (!req.body.cert && req.body.key) return next(new HttpError(400, 'cert must be provided'));
apps.setCertificate(req.resource, req.body, auditSource.fromRequest(req), function (error) {
apps.setCertificate(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -298,12 +278,12 @@ function setCertificate(req, res, next) {
function setEnvironment(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (!req.body.env || typeof req.body.env !== 'object') return next(new HttpError(400, 'env must be an object'));
if (Object.keys(req.body.env).some((key) => typeof req.body.env[key] !== 'string')) return next(new HttpError(400, 'env must contain values as strings'));
apps.setEnvironment(req.resource, req.body.env, auditSource.fromRequest(req), function (error, result) {
apps.setEnvironment(req.params.id, req.body.env, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -312,11 +292,11 @@ function setEnvironment(req, res, next) {
function setDebugMode(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.debugMode !== null && typeof req.body.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
apps.setDebugMode(req.resource, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
apps.setDebugMode(req.params.id, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -325,12 +305,12 @@ function setDebugMode(req, res, next) {
function setMailbox(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.mailboxName !== null && typeof req.body.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
if (typeof req.body.mailboxDomain !== 'string') return next(new HttpError(400, 'mailboxDomain must be a string'));
apps.setMailbox(req.resource, req.body.mailboxName, req.body.mailboxDomain, auditSource.fromRequest(req), function (error, result) {
apps.setMailbox(req.params.id, req.body.mailboxName, req.body.mailboxDomain, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -339,7 +319,7 @@ function setMailbox(req, res, next) {
function setLocation(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.body.location !== 'string') return next(new HttpError(400, 'location must be string')); // location may be an empty string
if (!req.body.domain) return next(new HttpError(400, 'domain is required'));
@@ -354,7 +334,7 @@ function setLocation(req, res, next) {
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.setLocation(req.resource, req.body, auditSource.fromRequest(req), function (error, result) {
apps.setLocation(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -363,49 +343,47 @@ function setLocation(req, res, next) {
function setDataDir(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.dataDir !== null && typeof req.body.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
apps.setDataDir(req.resource, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
apps.setDataDir(req.params.id, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function repair(req, res, next) {
function repairApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
const data = req.body;
if ('manifest' in data) {
if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
if (safe.query(data.manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to repair app with docker addon'));
}
if ('dockerImage' in data) {
if (!data.dockerImage || typeof data.dockerImage !== 'string') return next(new HttpError(400, 'dockerImage must be a string'));
}
apps.repair(req.resource, data, auditSource.fromRequest(req), function (error, result) {
apps.repair(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function restore(req, res, next) {
function restoreApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var data = req.body;
if (!data.backupId || typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be non-empty string'));
apps.restore(req.resource, data.backupId, auditSource.fromRequest(req), function (error, result) {
apps.restore(req.params.id, data.backupId, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -414,7 +392,7 @@ function restore(req, res, next) {
function importApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var data = req.body;
@@ -428,7 +406,7 @@ function importApp(req, res, next) {
if (req.body.backupConfig) {
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('password' in backupConfig && typeof backupConfig.password !== 'string') return next(new HttpError(400, 'password must be a string'));
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
// testing backup config can take sometime
@@ -436,16 +414,16 @@ function importApp(req, res, next) {
}
}
apps.importApp(req.resource, data, auditSource.fromRequest(req), function (error, result) {
apps.importApp(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function clone(req, res, next) {
function cloneApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var data = req.body;
@@ -456,66 +434,66 @@ function clone(req, res, next) {
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.clone(req.resource, data, req.user, auditSource.fromRequest(req), function (error, result) {
apps.clone(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { id: result.id, taskId: result.taskId }));
});
}
function backup(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
function backupApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.backup(req.resource, function (error, result) {
apps.backup(req.params.id, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function uninstall(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
function uninstallApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.uninstall(req.resource, auditSource.fromRequest(req), function (error, result) {
apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function start(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
function startApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.start(req.resource, auditSource.fromRequest(req), function (error, result) {
apps.start(req.params.id, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function stop(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
function stopApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.stop(req.resource, auditSource.fromRequest(req), function (error, result) {
apps.stop(req.params.id, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function restart(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
function restartApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.restart(req.resource, auditSource.fromRequest(req), function (error, result) {
apps.restart(req.params.id, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function update(req, res, next) {
function updateApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
var data = req.body;
@@ -527,24 +505,16 @@ function update(req, res, next) {
if ('skipBackup' in data && typeof data.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to update app with docker addon'));
data.appStoreId = appStoreId;
data.manifest = manifest;
apps.update(req.resource, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
// this route is for streaming logs
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
@@ -559,7 +529,7 @@ function getLogStream(req, res, next) {
format: 'json'
};
apps.getLogs(req.resource, options, function (error, logStream) {
apps.getLogs(req.params.id, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
@@ -581,7 +551,7 @@ function getLogStream(req, res, next) {
}
function getLogs(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10;
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
@@ -592,7 +562,7 @@ function getLogs(req, res, next) {
format: req.query.format || 'json'
};
apps.getLogs(req.resource, options, function (error, logStream) {
apps.getLogs(req.params.id, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
@@ -628,7 +598,7 @@ function demuxStream(stream, stdin) {
}
function exec(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var cmd = null;
if (req.query.cmd) {
@@ -642,16 +612,13 @@ function exec(req, res, next) {
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
var tty = req.query.tty === 'true';
var tty = req.query.tty === 'true' ? true : false;
if (safe.query(req.resource, 'manifest.addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is requied to exec app with docker addon'));
// in a badly configured reverse proxy, we might be here without an upgrade
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(BoxError.toHttpError(error));
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
req.clearTimeout();
res.sendUpgradeHandshake();
@@ -669,7 +636,7 @@ function exec(req, res, next) {
}
function execWebSocket(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var cmd = null;
if (req.query.cmd) {
@@ -685,10 +652,7 @@ function execWebSocket(req, res, next) {
var tty = req.query.tty === 'true' ? true : false;
// in a badly configured reverse proxy, we might be here without an upgrade
if (req.headers['upgrade'] !== 'websocket') return next(new HttpError(404, 'exec requires websocket'));
apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(BoxError.toHttpError(error));
req.clearTimeout();
@@ -718,7 +682,7 @@ function execWebSocket(req, res, next) {
}
function listBackups(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
@@ -726,7 +690,7 @@ function listBackups(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
apps.listBackups(req.resource, page, perPage, function (error, result) {
apps.listBackups(page, perPage, req.params.id, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { backups: result }));
@@ -734,12 +698,12 @@ function listBackups(req, res, next) {
}
function uploadFile(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart'));
apps.uploadFile(req.resource, req.files.file.path, req.query.file, function (error) {
apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
@@ -747,11 +711,11 @@ function uploadFile(req, res, next) {
}
function downloadFile(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
apps.downloadFile(req.resource, req.query.file, function (error, stream, info) {
apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) {
if (error) return next(BoxError.toHttpError(error));
var headers = {
@@ -765,23 +729,3 @@ function downloadFile(req, res, next) {
stream.pipe(res);
});
}
function setBinds(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
if (!req.body.binds || typeof req.body.binds !== 'object') return next(new HttpError(400, 'binds should be an object'));
for (let name of Object.keys(req.body.binds)) {
if (!req.body.binds[name] || typeof req.body.binds[name] !== 'object') return next(new HttpError(400, 'each bind should be an object'));
if (typeof req.body.binds[name].hostPath !== 'string') return next(new HttpError(400, 'hostPath must be a string'));
if (typeof req.body.binds[name].readOnly !== 'boolean') return next(new HttpError(400, 'readOnly must be a boolean'));
}
apps.setBinds(req.resource, req.body.binds, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
+2 -5
View File
@@ -102,7 +102,6 @@ function passwordReset(req, res, next) {
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid resetToken'));
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
if (!userObject.username) return next(new HttpError(409, 'No username set'));
// setPassword clears the resetToken
@@ -131,8 +130,6 @@ function setupAccount(req, res, next) {
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid Reset Token'));
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
users.update(userObject, { username: req.body.username, displayName: req.body.displayName }, auditSource.fromRequest(req), function (error) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return next(new HttpError(409, 'Username already used'));
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
@@ -218,8 +215,8 @@ function checkForUpdates(req, res, next) {
req.clearTimeout();
async.series([
(done) => updateChecker.checkAppUpdates({ automatic: false }, done),
(done) => updateChecker.checkBoxUpdates({ automatic: false }, done),
updateChecker.checkAppUpdates,
updateChecker.checkBoxUpdates
], function () {
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
});
+45 -8
View File
@@ -2,6 +2,8 @@
exports = module.exports = {
getDomain: getDomain,
addDomain: addDomain,
removeDomain: removeDomain,
setDnsRecords: setDnsRecords,
@@ -20,6 +22,7 @@ exports = module.exports = {
updateMailbox: updateMailbox,
removeMailbox: removeMailbox,
listAliases: listAliases,
getAliases: getAliases,
setAliases: setAliases,
@@ -47,6 +50,18 @@ function getDomain(req, res, next) {
});
}
function addDomain(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
mail.addDomain(req.body.domain, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { domain: req.body.domain }));
});
}
function setDnsRecords(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.domain, 'string');
@@ -62,6 +77,16 @@ function setDnsRecords(req, res, next) {
});
}
function removeDomain(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.removeDomain(req.params.domain, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
}
function getStatus(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
@@ -214,6 +239,22 @@ function removeMailbox(req, res, next) {
});
}
function listAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a positive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number'));
mail.listAliases(req.params.domain, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { aliases: result }));
});
}
function getAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
@@ -232,10 +273,8 @@ function setAliases(req, res, next) {
if (!Array.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array'));
for (let alias of req.body.aliases) {
if (!alias || typeof alias !== 'object') return next(new HttpError(400, 'each alias must have a name and domain'));
if (typeof alias.name !== 'string') return next(new HttpError(400, 'name must be a string'));
if (typeof alias.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
for (var i = 0; i < req.body.aliases.length; i++) {
if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string'));
}
mail.setAliases(req.params.name, req.params.domain, req.body.aliases, function (error) {
@@ -277,9 +316,8 @@ function addList(req, res, next) {
for (var i = 0; i < req.body.members.length; i++) {
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
}
if (typeof req.body.membersOnly !== 'boolean') return next(new HttpError(400, 'membersOnly must be a boolean'));
mail.addList(req.body.name, req.params.domain, req.body.members, req.body.membersOnly, auditSource.fromRequest(req), function (error) {
mail.addList(req.body.name, req.params.domain, req.body.members, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, {}));
@@ -296,9 +334,8 @@ function updateList(req, res, next) {
for (var i = 0; i < req.body.members.length; i++) {
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
}
if (typeof req.body.membersOnly !== 'boolean') return next(new HttpError(400, 'membersOnly must be a boolean'));
mail.updateList(req.params.name, req.params.domain, req.body.members, req.body.membersOnly, auditSource.fromRequest(req), function (error) {
mail.updateList(req.params.name, req.params.domain, req.body.members, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
+1 -1
View File
@@ -21,7 +21,7 @@ function proxy(req, res, next) {
delete req.headers['authorization'];
delete req.headers['cookies'];
addons.getContainerDetails('mail', 'CLOUDRON_MAIL_TOKEN', function (error, addonDetails) {
addons.getServiceDetails('mail', 'CLOUDRON_MAIL_TOKEN', function (error, addonDetails) {
if (error) return next(BoxError.toHttpError(error));
parsedUrl.query['access_token'] = addonDetails.token;
+2 -2
View File
@@ -98,11 +98,11 @@ function restore(req, res, next) {
var backupConfig = req.body.backupConfig;
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('password' in backupConfig && typeof backupConfig.password !== 'string') return next(new HttpError(400, 'password must be a string'));
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (typeof backupConfig.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string'));
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
if ('sysinfoConfig' in req.body && typeof req.body.sysinfoConfig !== 'object') return next(new HttpError(400, 'sysinfoConfig must be an object'));
+2 -3
View File
@@ -97,8 +97,9 @@ 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.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
if (typeof req.body.intervalSecs !== 'number') return next(new HttpError(400, 'intervalSecs is required'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('syncConcurrency' in req.body) {
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
if (req.body.syncConcurrency < 1) return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
@@ -106,8 +107,6 @@ function setBackupConfig(req, res, next) {
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'));
if (!req.body.retentionPolicy || typeof req.body.retentionPolicy !== 'object') return next(new HttpError(400, 'retentionPolicy is required'));
// testing the backup using put/del takes a bit of time at times
req.clearTimeout();
+1 -1
View File
@@ -62,7 +62,7 @@ function setup(done) {
},
function createSettings(callback) {
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz', retentionPolicy: {} }, callback);
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' }, callback);
}
], 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: {} })
settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' })
], done);
}
+272 -56
View File
@@ -63,6 +63,8 @@ function setup(done) {
.end(function (error, result) {
if (!result || result.statusCode !== 200) return retryCallback(new Error('Bad result'));
console.dir(result.body);
if (!result.body.setup.active && result.body.setup.errorMessage === '' && result.body.adminFqdn) return retryCallback();
retryCallback(new Error('Not done yet: ' + JSON.stringify(result.body)));
@@ -122,6 +124,46 @@ describe('Mail API', function () {
after(cleanup);
describe('crud', function () {
it('cannot add non-existing domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: 'doesnotexist.com' })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('domain must be a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: ['doesnotexist.com'] })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('can add domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
it('cannot add domain twice', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('cannot get non-existing domain', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.query({ access_token: token })
@@ -146,6 +188,33 @@ describe('Mail API', function () {
done();
});
});
it('cannot delete non-existing domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('cannot delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + ADMIN_DOMAIN.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('can delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
});
describe('status', function () {
@@ -174,13 +243,20 @@ describe('Mail API', function () {
mxDomain = DOMAIN_0.domain;
dmarcDomain = '_dmarc.' + DOMAIN_0.domain;
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/enable')
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ enabled: true })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.statusCode).to.equal(201);
done();
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/enable')
.query({ access_token: token })
.send({ enabled: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
});
});
@@ -189,7 +265,12 @@ describe('Mail API', function () {
dns.resolve = resolve;
done();
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('does not fail when dns errors', function (done) {
@@ -298,7 +379,7 @@ describe('Mail API', function () {
it('succeeds with all different spf, dkim, dmarc, mx, ptr records', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: settings.mailFqdn() }, { priority: '10', exchange: 'some.other.server' } ];
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: settings.mailFqdn() }, { priority: '30', exchange: settings.mailFqdn() } ];
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC2; p=reject; pct=100']];
dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain)]];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:random.com ~all']];
@@ -315,7 +396,7 @@ describe('Mail API', function () {
expect(res.body.dns.dkim).to.be.an('object');
expect(res.body.dns.dkim.expected).to.eql('v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
expect(res.body.dns.dkim.status).to.eql(true); // as long as p= matches we are good
expect(res.body.dns.dkim.status).to.eql(false);
expect(res.body.dns.dkim.value).to.eql('v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
expect(res.body.dns.dmarc).to.be.an('object');
@@ -324,9 +405,9 @@ describe('Mail API', function () {
expect(res.body.dns.dmarc.value).to.eql('v=DMARC2; p=reject; pct=100');
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(true);
expect(res.body.dns.mx.status).to.eql(false);
expect(res.body.dns.mx.expected).to.eql('10 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('20 ' + settings.mailFqdn() + '. 10 some.other.server.');
expect(res.body.dns.mx.value).to.eql('20 ' + settings.mailFqdn() + '. 30 ' + settings.mailFqdn() + '.');
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.expected).to.eql(settings.mailFqdn());
@@ -422,6 +503,25 @@ describe('Mail API', function () {
});
describe('mail from validation', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('get mail from validation succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
@@ -454,6 +554,25 @@ describe('Mail API', function () {
});
describe('catch_all', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('get catch_all succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
@@ -505,6 +624,25 @@ describe('Mail API', function () {
});
describe('mail relay', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('get mail relay succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
@@ -563,6 +701,25 @@ describe('Mail API', function () {
});
describe('mailboxes', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('add succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes')
.send({ name: MAILBOX_NAME, userId: userId })
@@ -600,8 +757,7 @@ describe('Mail API', function () {
expect(res.body.mailbox).to.be.an('object');
expect(res.body.mailbox.name).to.equal(MAILBOX_NAME);
expect(res.body.mailbox.ownerId).to.equal(userId);
expect(res.body.mailbox.aliasName).to.equal(null);
expect(res.body.mailbox.aliasDomain).to.equal(null);
expect(res.body.mailbox.aliasTarget).to.equal(null);
expect(res.body.mailbox.domain).to.equal(DOMAIN_0.domain);
done();
});
@@ -616,8 +772,7 @@ describe('Mail API', function () {
expect(res.body.mailboxes[0]).to.be.an('object');
expect(res.body.mailboxes[0].name).to.equal(MAILBOX_NAME);
expect(res.body.mailboxes[0].ownerId).to.equal(userId);
expect(res.body.mailboxes[0].aliasName).to.equal(null);
expect(res.body.mailboxes[0].aliasDomain).to.equal(null);
expect(res.body.mailboxes[0].aliasTarget).to.equal(null);
expect(res.body.mailboxes[0].domain).to.equal(DOMAIN_0.domain);
done();
});
@@ -648,14 +803,69 @@ describe('Mail API', function () {
});
describe('aliases', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
if (error) return done(error);
done();
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
});
it('add the mailbox', function (done) {
it('set fails if aliases is missing', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set fails if user does not exist', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + 'someuserdoesnotexist')
.send({ aliases: ['hello', 'there'] })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('set fails if aliases is the wrong type', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
.send({ aliases: 'hello, there' })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set fails if user is not enabled', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
.send({ aliases: ['hello', 'there'] })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('now add the mailbox', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes')
.send({ name: MAILBOX_NAME, userId: userId })
.query({ access_token: token })
@@ -665,38 +875,9 @@ describe('Mail API', function () {
});
});
it('set fails if aliases is missing', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + MAILBOX_NAME + '/aliases')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set fails if user does not exist', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/randomuser/aliases')
.send({ aliases: [{ name: 'hello', domain: DOMAIN_0.domain}, {name: 'there', domain: DOMAIN_0.domain}] })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('set fails if aliases is the wrong type', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + MAILBOX_NAME + '/aliases')
.send({ aliases: 'hello, there' })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + MAILBOX_NAME + '/aliases')
.send({ aliases: [{ name: 'hello', domain: DOMAIN_0.domain}, {name: 'there', domain: DOMAIN_0.domain}] })
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
.send({ aliases: ['hello', 'there'] })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
@@ -705,17 +886,35 @@ describe('Mail API', function () {
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + MAILBOX_NAME + '/aliases')
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.aliases).to.eql([{ name: 'hello', domain: DOMAIN_0.domain}, {name: 'there', domain: DOMAIN_0.domain}]);
expect(res.body.aliases).to.eql(['hello', 'there']);
done();
});
});
it('listing succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.aliases.length).to.eql(2);
expect(res.body.aliases[0].name).to.equal('hello');
expect(res.body.aliases[0].ownerId).to.equal(userId);
expect(res.body.aliases[0].aliasTarget).to.equal(MAILBOX_NAME);
expect(res.body.aliases[0].domain).to.equal(DOMAIN_0.domain);
expect(res.body.aliases[1].name).to.equal('there');
expect(res.body.aliases[1].ownerId).to.equal(userId);
expect(res.body.aliases[1].aliasTarget).to.equal(MAILBOX_NAME);
expect(res.body.aliases[1].domain).to.equal(DOMAIN_0.domain);
done();
});
});
it('get fails if mailbox does not exist', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/somerandomuser/aliases')
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + 'someuserdoesnotexist')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
@@ -725,11 +924,30 @@ describe('Mail API', function () {
});
describe('mailinglists', function () {
before(function (done) {
async.series([
function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
}
], done);
});
after(function (done) {
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
if (error) return done(error);
done();
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
});
@@ -764,7 +982,7 @@ describe('Mail API', function () {
it('add succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists')
.send({ name: LIST_NAME, members: [ `admin2@${DOMAIN_0.domain}`, `${USERNAME}@${DOMAIN_0.domain}`], membersOnly: false })
.send({ name: LIST_NAME, members: [ `admin2@${DOMAIN_0.domain}`, `${USERNAME}@${DOMAIN_0.domain}`] })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
@@ -774,7 +992,7 @@ describe('Mail API', function () {
it('add twice fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists')
.send({ name: LIST_NAME, members: [ `admin2@${DOMAIN_0.domain}`, `${USERNAME}@${DOMAIN_0.domain}`], membersOnly: false })
.send({ name: LIST_NAME, members: [ `admin2@${DOMAIN_0.domain}`, `${USERNAME}@${DOMAIN_0.domain}`] })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
@@ -799,10 +1017,9 @@ describe('Mail API', function () {
expect(res.body.list).to.be.an('object');
expect(res.body.list.name).to.equal(LIST_NAME);
expect(res.body.list.ownerId).to.equal('admin');
expect(res.body.list.aliasName).to.equal(null);
expect(res.body.list.aliasTarget).to.equal(null);
expect(res.body.list.domain).to.equal(DOMAIN_0.domain);
expect(res.body.list.members).to.eql([ `admin2@${DOMAIN_0.domain}`, `superadmin@${DOMAIN_0.domain}` ]);
expect(res.body.list.membersOnly).to.be(false);
done();
});
});
@@ -816,10 +1033,9 @@ describe('Mail API', function () {
expect(res.body.lists.length).to.equal(1);
expect(res.body.lists[0].name).to.equal(LIST_NAME);
expect(res.body.lists[0].ownerId).to.equal('admin');
expect(res.body.lists[0].aliasName).to.equal(null);
expect(res.body.lists[0].aliasTarget).to.equal(null);
expect(res.body.lists[0].domain).to.equal(DOMAIN_0.domain);
expect(res.body.lists[0].members).to.eql([ `admin2@${DOMAIN_0.domain}`, `superadmin@${DOMAIN_0.domain}` ]);
expect(res.body.lists[0].membersOnly).to.be(false);
done();
});
});
+1
View File
@@ -47,6 +47,7 @@ function setup(done) {
server.start,
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
mail.addDomain.bind(null, DOMAIN_0.domain)
], function (error) {
expect(error).to.not.be.ok();
+22 -1
View File
@@ -23,6 +23,8 @@ function sync(callback) {
callback = callback || NOOP_CALLBACK;
debug('sync: synchronizing global state with installed app state');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
@@ -47,6 +49,8 @@ function sync(callback) {
return iteratorDone(); // nothing changed
}
debug(`sync: app ${app.fqdn} changed`);
stopJobs(app.id, appState, function (error) {
if (error) debug(`sync: error stopping jobs of ${app.fqdn} : ${error.message}`);
@@ -63,6 +67,8 @@ function sync(callback) {
iteratorDone();
});
});
debug('sync: done');
});
});
}
@@ -86,6 +92,8 @@ function stopJobs(appId, appState, callback) {
assert.strictEqual(typeof appState, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`stopJobs: stopping jobs of ${appId}`);
if (!appState) return callback();
async.eachSeries(Object.keys(appState.schedulerConfig), function (taskName, iteratorDone) {
@@ -101,6 +109,8 @@ function createCronJobs(app, schedulerConfig) {
assert.strictEqual(typeof app, 'object');
assert(schedulerConfig && typeof schedulerConfig === 'object');
debug(`createCronJobs: creating cron jobs for app ${app.fqdn}`);
var jobs = { };
Object.keys(schedulerConfig).forEach(function (taskName) {
@@ -110,6 +120,8 @@ function createCronJobs(app, schedulerConfig) {
var cronTime = (constants.TEST ? '*/5 ' : `${randomSecond} `) + task.schedule; // time ticks faster in tests
debug(`createCronJobs: ${app.fqdn} task ${taskName} scheduled at ${cronTime} with cmd ${task.command}`);
var cronJob = new CronJob({
cronTime: cronTime, // at this point, the pattern has been validated
onTick: runTask.bind(null, app.id, taskName), // put the app id in closure, so we don't use the outdated app object by mistake
@@ -131,6 +143,8 @@ function runTask(appId, taskName, callback) {
callback = callback || NOOP_CALLBACK;
debug(`runTask: running task ${taskName} of ${appId}`);
apps.get(appId, function (error, app) {
if (error) return callback(error);
@@ -144,13 +158,20 @@ function runTask(appId, taskName, callback) {
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();
if (new Date() - jobStartTime < JOB_MAX_TIME) {
debug(`runTask: skipped task ${taskName} of app ${app.fqdn} since it was started at ${jobStartTime}`);
return callback();
}
}
debug(`runTask: removing any old task ${taskName} of app ${app.fqdn}`);
killContainer(containerName, function (error) {
if (error) return callback(error);
const cmd = gState[appId].schedulerConfig[taskName].command;
debug(`runTask: starting task ${taskName} of app ${app.fqdn} with cmd ${cmd}`);
// 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);
+41 -46
View File
@@ -191,46 +191,45 @@ function initializeExpressSync() {
// app routes
router.get ('/api/v1/apps', token, routes.apps.getApps);
router.get ('/api/v1/apps/:id', token, authorizeAdmin, routes.apps.load, routes.apps.getApp);
router.get ('/api/v1/apps/:id/icon', token, routes.apps.load, routes.apps.getAppIcon);
router.get ('/api/v1/apps/:id', token, authorizeAdmin, routes.apps.getApp);
router.get ('/api/v1/apps/:id/icon', token, routes.apps.getAppIcon);
router.post('/api/v1/apps/install', token, authorizeAdmin, routes.apps.install);
router.post('/api/v1/apps/:id/uninstall', token, authorizeAdmin, routes.apps.load, routes.apps.uninstall);
router.post('/api/v1/apps/install', token, authorizeAdmin, routes.apps.installApp);
router.post('/api/v1/apps/:id/uninstall', token, authorizeAdmin, routes.apps.uninstallApp);
router.post('/api/v1/apps/:id/configure/access_restriction', token, authorizeAdmin, routes.apps.load, routes.apps.setAccessRestriction);
router.post('/api/v1/apps/:id/configure/label', token, authorizeAdmin, routes.apps.load, routes.apps.setLabel);
router.post('/api/v1/apps/:id/configure/tags', token, authorizeAdmin, routes.apps.load, routes.apps.setTags);
router.post('/api/v1/apps/:id/configure/icon', token, authorizeAdmin, routes.apps.load, routes.apps.setIcon);
router.post('/api/v1/apps/:id/configure/memory_limit', token, authorizeAdmin, routes.apps.load, routes.apps.setMemoryLimit);
router.post('/api/v1/apps/:id/configure/cpu_shares', token, authorizeAdmin, routes.apps.load, routes.apps.setCpuShares);
router.post('/api/v1/apps/:id/configure/automatic_backup', token, authorizeAdmin, routes.apps.load, routes.apps.setAutomaticBackup);
router.post('/api/v1/apps/:id/configure/automatic_update', token, authorizeAdmin, routes.apps.load, routes.apps.setAutomaticUpdate);
router.post('/api/v1/apps/:id/configure/reverse_proxy', token, authorizeAdmin, routes.apps.load, routes.apps.setReverseProxyConfig);
router.post('/api/v1/apps/:id/configure/cert', token, authorizeAdmin, routes.apps.load, routes.apps.setCertificate);
router.post('/api/v1/apps/:id/configure/debug_mode', token, authorizeAdmin, routes.apps.load, routes.apps.setDebugMode);
router.post('/api/v1/apps/:id/configure/mailbox', token, authorizeAdmin, routes.apps.load, routes.apps.setMailbox);
router.post('/api/v1/apps/:id/configure/env', token, authorizeAdmin, routes.apps.load, routes.apps.setEnvironment);
router.post('/api/v1/apps/:id/configure/data_dir', token, authorizeAdmin, routes.apps.load, routes.apps.setDataDir);
router.post('/api/v1/apps/:id/configure/location', token, authorizeAdmin, routes.apps.load, routes.apps.setLocation);
router.post('/api/v1/apps/:id/configure/binds', token, authorizeAdmin, routes.apps.load, routes.apps.setBinds);
router.post('/api/v1/apps/:id/configure/access_restriction', token, authorizeAdmin, routes.apps.setAccessRestriction);
router.post('/api/v1/apps/:id/configure/label', token, authorizeAdmin, routes.apps.setLabel);
router.post('/api/v1/apps/:id/configure/tags', token, authorizeAdmin, routes.apps.setTags);
router.post('/api/v1/apps/:id/configure/icon', token, authorizeAdmin, routes.apps.setIcon);
router.post('/api/v1/apps/:id/configure/memory_limit', token, authorizeAdmin, routes.apps.setMemoryLimit);
router.post('/api/v1/apps/:id/configure/cpu_shares', token, authorizeAdmin, routes.apps.setCpuShares);
router.post('/api/v1/apps/:id/configure/automatic_backup', token, authorizeAdmin, routes.apps.setAutomaticBackup);
router.post('/api/v1/apps/:id/configure/automatic_update', token, authorizeAdmin, routes.apps.setAutomaticUpdate);
router.post('/api/v1/apps/:id/configure/reverse_proxy', token, authorizeAdmin, routes.apps.setReverseProxyConfig);
router.post('/api/v1/apps/:id/configure/cert', token, authorizeAdmin, routes.apps.setCertificate);
router.post('/api/v1/apps/:id/configure/debug_mode', token, authorizeAdmin, routes.apps.setDebugMode);
router.post('/api/v1/apps/:id/configure/mailbox', token, authorizeAdmin, routes.apps.setMailbox);
router.post('/api/v1/apps/:id/configure/env', token, authorizeAdmin, routes.apps.setEnvironment);
router.post('/api/v1/apps/:id/configure/data_dir', token, authorizeAdmin, routes.apps.setDataDir);
router.post('/api/v1/apps/:id/configure/location', token, authorizeAdmin, routes.apps.setLocation);
router.post('/api/v1/apps/:id/repair', token, authorizeAdmin, routes.apps.load, routes.apps.repair);
router.post('/api/v1/apps/:id/update', token, authorizeAdmin, routes.apps.load, routes.apps.update);
router.post('/api/v1/apps/:id/restore', token, authorizeAdmin, routes.apps.load, routes.apps.restore);
router.post('/api/v1/apps/:id/import', token, authorizeAdmin, routes.apps.load, routes.apps.importApp);
router.post('/api/v1/apps/:id/backup', token, authorizeAdmin, routes.apps.load, routes.apps.backup);
router.get ('/api/v1/apps/:id/backups', token, authorizeAdmin, routes.apps.load, routes.apps.listBackups);
router.post('/api/v1/apps/:id/start', token, authorizeAdmin, routes.apps.load, routes.apps.start);
router.post('/api/v1/apps/:id/stop', token, authorizeAdmin, routes.apps.load, routes.apps.stop);
router.post('/api/v1/apps/:id/restart', token, authorizeAdmin, routes.apps.load, routes.apps.restart);
router.get ('/api/v1/apps/:id/logstream', token, authorizeAdmin, routes.apps.load, routes.apps.getLogStream);
router.get ('/api/v1/apps/:id/logs', token, authorizeAdmin, routes.apps.load, routes.apps.getLogs);
router.get ('/api/v1/apps/:id/exec', token, authorizeAdmin, routes.apps.load, routes.apps.exec);
router.post('/api/v1/apps/:id/repair', token, authorizeAdmin, routes.apps.repairApp);
router.post('/api/v1/apps/:id/update', token, authorizeAdmin, routes.apps.updateApp);
router.post('/api/v1/apps/:id/restore', token, authorizeAdmin, routes.apps.restoreApp);
router.post('/api/v1/apps/:id/import', token, authorizeAdmin, routes.apps.importApp);
router.post('/api/v1/apps/:id/backup', token, authorizeAdmin, routes.apps.backupApp);
router.get ('/api/v1/apps/:id/backups', token, authorizeAdmin, routes.apps.listBackups);
router.post('/api/v1/apps/:id/stop', token, authorizeAdmin, routes.apps.stopApp);
router.post('/api/v1/apps/:id/start', token, authorizeAdmin, routes.apps.startApp);
router.post('/api/v1/apps/:id/restart', token, authorizeAdmin, routes.apps.restartApp);
router.get ('/api/v1/apps/:id/logstream', token, authorizeAdmin, routes.apps.getLogStream);
router.get ('/api/v1/apps/:id/logs', token, authorizeAdmin, routes.apps.getLogs);
router.get ('/api/v1/apps/:id/exec', token, authorizeAdmin, routes.apps.exec);
// websocket cannot do bearer authentication
router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, users.ROLE_ADMIN), routes.apps.load, routes.apps.execWebSocket);
router.post('/api/v1/apps/:id/clone', token, authorizeAdmin, routes.apps.load, routes.apps.clone);
router.get ('/api/v1/apps/:id/download', token, authorizeAdmin, routes.apps.load, routes.apps.downloadFile);
router.post('/api/v1/apps/:id/upload', token, authorizeAdmin, multipart, routes.apps.load, routes.apps.uploadFile);
router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, users.ROLE_ADMIN), routes.apps.execWebSocket);
router.post('/api/v1/apps/:id/clone', token, authorizeAdmin, routes.apps.cloneApp);
router.get ('/api/v1/apps/:id/download', token, authorizeAdmin, routes.apps.downloadFile);
router.post('/api/v1/apps/:id/upload', token, authorizeAdmin, multipart, routes.apps.uploadFile);
router.get ('/api/v1/branding/:setting', token, authorizeOwner, routes.branding.get);
router.post('/api/v1/branding/:setting', token, authorizeOwner, (req, res, next) => {
@@ -245,15 +244,11 @@ function initializeExpressSync() {
}, routes.settings.set);
// email routes
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/mailserver/:pathname', token, authorizeAdmin, routes.mailserver.proxy);
router.get ('/api/v1/mail/:domain', token, authorizeAdmin, routes.mail.getDomain);
router.post('/api/v1/mail', token, authorizeAdmin, routes.mail.addDomain);
router.del ('/api/v1/mail/:domain', token, authorizeAdmin, routes.mail.removeDomain);
router.get ('/api/v1/mail/:domain/status', token, authorizeAdmin, routes.mail.getStatus);
router.post('/api/v1/mail/:domain/mail_from_validation', token, authorizeAdmin, routes.mail.setMailFromValidation);
router.post('/api/v1/mail/:domain/catch_all', token, authorizeAdmin, routes.mail.setCatchAllAddress);
@@ -266,9 +261,9 @@ function initializeExpressSync() {
router.post('/api/v1/mail/:domain/mailboxes', token, authorizeAdmin, routes.mail.addMailbox);
router.post('/api/v1/mail/:domain/mailboxes/:name', token, authorizeAdmin, routes.mail.updateMailbox);
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', token, authorizeAdmin, routes.mail.setAliases);
router.get ('/api/v1/mail/:domain/aliases', token, authorizeAdmin, routes.mail.listAliases);
router.get ('/api/v1/mail/:domain/aliases/:name', token, authorizeAdmin, routes.mail.getAliases);
router.put ('/api/v1/mail/:domain/aliases/:name', token, authorizeAdmin, routes.mail.setAliases);
router.get ('/api/v1/mail/:domain/lists', token, authorizeAdmin, routes.mail.getLists);
router.post('/api/v1/mail/:domain/lists', token, authorizeAdmin, routes.mail.addList);
router.get ('/api/v1/mail/:domain/lists/:name', token, authorizeAdmin, routes.mail.getList);
+6 -11
View File
@@ -142,10 +142,10 @@ let gDefaults = (function () {
result[exports.CLOUDRON_TOKEN_KEY] = '';
result[exports.BACKUP_CONFIG_KEY] = {
provider: 'filesystem',
key: '',
backupFolder: '/var/backups',
format: 'tgz',
encryption: null,
retentionPolicy: { keepWithinSecs: 2 * 24 * 60 * 60 }, // 2 days
retentionSecs: 2 * 24 * 60 * 60, // 2 days
intervalSecs: 24 * 60 * 60 // ~1 day
};
result[exports.PLATFORM_CONFIG_KEY] = {};
@@ -386,7 +386,7 @@ function getBackupConfig(callback) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.BACKUP_CONFIG_KEY]);
if (error) return callback(error);
callback(null, JSON.parse(value)); // provider, token, password, region, prefix, bucket
callback(null, JSON.parse(value)); // provider, token, key, region, prefix, bucket
});
}
@@ -394,19 +394,14 @@ function setBackupConfig(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
getBackupConfig(function (error, currentConfig) {
getBackupConfig(function (error, curentConfig) {
if (error) return callback(error);
backups.injectPrivateFields(backupConfig, currentConfig);
backups.injectPrivateFields(backupConfig, curentConfig);
backups.testConfig(backupConfig, function (error) {
if (error) return callback(error);
if ('password' in backupConfig) { // user set password
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password);
delete backupConfig.password;
}
backups.cleanupCacheFilesSync();
settingsdb.set(exports.BACKUP_CONFIG_KEY, JSON.stringify(backupConfig), function (error) {
@@ -428,7 +423,7 @@ function setBackupCredentials(credentials, callback) {
if (error) return callback(error);
// preserve these fields
const extra = _.pick(currentConfig, 'retentionPolicy', 'intervalSecs', 'copyConcurrency', 'syncConcurrency');
const extra = _.pick(currentConfig, 'retentionSecs', 'intervalSecs', 'copyConcurrency', 'syncConcurrency');
const backupConfig = _.extend({}, credentials, extra);
+3 -3
View File
@@ -21,8 +21,8 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
backups = require('../backups.js'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:storage/gcs'),
EventEmitter = require('events'),
GCS = require('@google-cloud/storage').Storage,
@@ -254,10 +254,10 @@ function testConfig(apiConfig, callback) {
}
function removePrivateFields(apiConfig) {
apiConfig.credentials.private_key = constants.SECRET_PLACEHOLDER;
apiConfig.credentials.private_key = backups.SECRET_PLACEHOLDER;
return apiConfig;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.credentials.private_key === constants.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
if (newConfig.credentials.private_key === backups.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
}
+2 -2
View File
@@ -32,13 +32,13 @@ var assert = require('assert'),
EventEmitter = require('events');
function removePrivateFields(apiConfig) {
// in-place removal of tokens and api keys with constants.SECRET_PLACEHOLDER
// in-place removal of tokens and api keys with domains.SECRET_PLACEHOLDER
return apiConfig;
}
// eslint-disable-next-line no-unused-vars
function injectPrivateFields(newConfig, currentConfig) {
// in-place injection of tokens and api keys which came in with constants.SECRET_PLACEHOLDER
// in-place injection of tokens and api keys which came in with domains.SECRET_PLACEHOLDER
}
function upload(apiConfig, backupFilePath, sourceStream, callback) {
+5 -6
View File
@@ -22,16 +22,15 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
AWS = require('aws-sdk'),
backups = require('../backups.js'),
BoxError = require('../boxerror.js'),
chunk = require('lodash.chunk'),
constants = require('../constants.js'),
debug = require('debug')('box:storage/s3'),
EventEmitter = require('events'),
https = require('https'),
PassThrough = require('stream').PassThrough,
path = require('path'),
S3BlockReadStream = require('s3-block-read-stream'),
_ = require('underscore');
S3BlockReadStream = require('s3-block-read-stream');
// test only
var originalAWS;
@@ -430,7 +429,7 @@ function testConfig(apiConfig, callback) {
Body: 'testcontent'
};
var s3 = new AWS.S3(_.omit(credentials, 'retryDelayOptions', 'maxRetries'));
var s3 = new AWS.S3(credentials);
s3.putObject(params, function (error) {
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message || error.code)); // DO sets 'code'
@@ -449,10 +448,10 @@ function testConfig(apiConfig, callback) {
}
function removePrivateFields(apiConfig) {
apiConfig.secretAccessKey = constants.SECRET_PLACEHOLDER;
apiConfig.secretAccessKey = backups.SECRET_PLACEHOLDER;
return apiConfig;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.secretAccessKey === constants.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
if (newConfig.secretAccessKey === backups.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
}
+1 -1
View File
@@ -40,7 +40,7 @@ function getDisks(callback) {
const ext4Disks = values[0].filter((r) => r.type === 'ext4').sort((a, b) => a.mountpoint.localeCompare(b.mountpoint));
const disks = {
disks: ext4Disks, // root disk is first. { filesystem, type, size, used, avialable, capacity, mountpoint }
disks: ext4Disks, // root disk is first
boxDataDisk: values[1].filesystem,
mailDataDisk: values[1].filesystem,
platformDataDisk: values[2].filesystem,
-9
View File
@@ -15,7 +15,6 @@ var appdb = require('../appdb.js'),
groupdb = require('../groupdb.js'),
groups = require('../groups.js'),
hat = require('../hat.js'),
settings = require('../settings.js'),
userdb = require('../userdb.js');
let AUDIT_SOURCE = { ip: '1.2.3.4' };
@@ -174,7 +173,6 @@ describe('Apps', function () {
async.series([
database.initialize,
database._clear,
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
domains.add.bind(null, DOMAIN_1.domain, DOMAIN_1, AUDIT_SOURCE),
userdb.add.bind(null, ADMIN_0.id, ADMIN_0),
@@ -210,13 +208,6 @@ describe('Apps', function () {
expect(apps._validatePortBindings({ port: 1567 }, { tcpPorts: { port3: null } })).to.be.an(Error);
});
it('does not allow reserved ports', function () {
expect(apps._validatePortBindings({ port: 443 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 50000 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 51000 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 50100 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
});
it('allows valid bindings', function () {
expect(apps._validatePortBindings({ port: 1024 }, { tcpPorts: { port: 5000 } })).to.be(null);
+21 -2
View File
@@ -18,7 +18,6 @@ var addons = require('../addons.js'),
net = require('net'),
nock = require('nock'),
paths = require('../paths.js'),
settings = require('../settings.js'),
userdb = require('../userdb.js'),
_ = require('underscore');
@@ -120,7 +119,6 @@ describe('apptask', function () {
async.series([
database.initialize,
database._clear,
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
userdb.add.bind(null, ADMIN.id, ADMIN),
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.domain, APP.portBindings, APP)
@@ -186,6 +184,27 @@ describe('apptask', function () {
});
});
it('allocate OAuth credentials', function (done) {
addons._setupOauth(APP, {}, function (error) {
expect(error).to.be(null);
done();
});
});
it('remove OAuth credentials', function (done) {
addons._teardownOauth(APP, {}, function (error) {
expect(error).to.be(null);
done();
});
});
it('remove OAuth credentials twice succeeds', function (done) {
addons._teardownOauth(APP, {}, function (error) {
expect(!error).to.be.ok();
done();
});
});
it('barfs on empty manifest', function (done) {
var badApp = _.extend({ }, APP);
badApp.manifest = { };
+20 -91
View File
@@ -17,7 +17,6 @@ var async = require('async'),
fs = require('fs'),
os = require('os'),
mkdirp = require('mkdirp'),
moment = require('moment'),
path = require('path'),
rimraf = require('rimraf'),
settings = require('../settings.js'),
@@ -69,69 +68,6 @@ function cleanupBackups(callback) {
});
}
describe('retention policy', function () {
it('always keeps if reason is set', function () {
let backup = { keepReason: 'somereason' };
backups._applyBackupRetentionPolicy([backup], { keepWithinSecs: 1 });
expect(backup.keepReason).to.be('somereason');
});
it('always keeps forever policy', function () {
let backup = { creationTime: new Date() };
backups._applyBackupRetentionPolicy([backup], { keepWithinSecs: -1 });
expect(backup.keepReason).to.be('keepWithinSecs');
});
it('preserveSecs takes precedence', function () {
let backup = { creationTime: new Date(), preserveSecs: 3000 };
backups._applyBackupRetentionPolicy([backup], { keepWithinSecs: 1 });
expect(backup.keepReason).to.be('preserveSecs');
});
it('1 daily', function () {
let b = [
{ id: '1', creationTime: moment().toDate() },
{ id: '2', creationTime: moment().subtract(3, 'h').toDate() },
{ id: '3', creationTime: moment().subtract(20, 'h').toDate() },
{ id: '4', creationTime: moment().subtract(5, 'd').toDate() }
];
backups._applyBackupRetentionPolicy(b, { keepDaily: 1 });
expect(b[0].keepReason).to.be('keepDaily');
expect(b[1].keepReason).to.be(undefined);
expect(b[2].keepReason).to.be(undefined);
expect(b[3].keepReason).to.be(undefined);
});
it('2 daily, 1 weekly', function () {
let b = [
{ id: '1', creationTime: moment().toDate() },
{ id: '2', creationTime: moment().subtract(3, 'h').toDate() },
{ id: '3', creationTime: moment().subtract(26, 'h').toDate() },
{ id: '4', creationTime: moment().subtract(5, 'd').toDate() }
];
backups._applyBackupRetentionPolicy(b, { keepDaily: 2, keepWeekly: 1 });
expect(b[0].keepReason).to.be('keepDaily'); // today
expect(b[1].keepReason).to.be('keepWeekly');
expect(b[2].keepReason).to.be('keepDaily'); // yesterday
expect(b[3].keepReason).to.be(undefined);
});
it('2 daily, 6 monthly, 1 yearly', function () {
let b = [
{ id: '1', creationTime: moment().toDate() },
{ id: '2', creationTime: moment().subtract(3, 'h').toDate() },
{ id: '3', creationTime: moment().subtract(26, 'h').toDate() },
{ id: '4', creationTime: moment().subtract(5, 'd').toDate() }
];
backups._applyBackupRetentionPolicy(b, { keepDaily: 2, keepMonthly: 6, keepYearly: 1 });
expect(b[0].keepReason).to.be('keepDaily'); // today
expect(b[1].keepReason).to.be('keepMonthly');
expect(b[2].keepReason).to.be('keepDaily'); // yesterday
expect(b[3].keepReason).to.be('keepYearly');
});
});
describe('backups', function () {
before(function (done) {
const BACKUP_DIR = path.join(os.tmpdir(), 'cloudron-backup-test');
@@ -142,9 +78,9 @@ describe('backups', function () {
database._clear,
settings.setBackupConfig.bind(null, {
provider: 'filesystem',
password: 'supersecret',
key: 'enckey',
backupFolder: BACKUP_DIR,
retentionPolicy: { keepWithinSecs: 1 },
retentionSecs: 1,
format: 'tgz'
})
], done);
@@ -159,8 +95,7 @@ describe('backups', function () {
describe('cleanup', function () {
var BACKUP_0 = {
id: 'backup-box-0',
encryptionVersion: null,
packageVersion: '1.0.0',
version: '1.0.0',
type: backupdb.BACKUP_TYPE_BOX,
dependsOn: [ 'backup-app-00', 'backup-app-01' ],
manifest: null,
@@ -169,8 +104,7 @@ describe('backups', function () {
var BACKUP_0_APP_0 = {
id: 'backup-app-00',
encryptionVersion: null,
packageVersion: '1.0.0',
version: '1.0.0',
type: backupdb.BACKUP_TYPE_APP,
dependsOn: [],
manifest: null,
@@ -179,8 +113,7 @@ describe('backups', function () {
var BACKUP_0_APP_1 = {
id: 'backup-app-01',
encryptionVersion: null,
packageVersion: '1.0.0',
version: '1.0.0',
type: backupdb.BACKUP_TYPE_APP,
dependsOn: [],
manifest: null,
@@ -189,8 +122,7 @@ describe('backups', function () {
var BACKUP_1 = {
id: 'backup-box-1',
encryptionVersion: null,
packageVersion: '1.0.0',
version: '1.0.0',
type: backupdb.BACKUP_TYPE_BOX,
dependsOn: [ 'backup-app-10', 'backup-app-11' ],
manifest: null,
@@ -199,8 +131,7 @@ describe('backups', function () {
var BACKUP_1_APP_0 = {
id: 'backup-app-10',
encryptionVersion: null,
packageVersion: '1.0.0',
version: '1.0.0',
type: backupdb.BACKUP_TYPE_APP,
dependsOn: [],
manifest: null,
@@ -209,8 +140,7 @@ describe('backups', function () {
var BACKUP_1_APP_1 = {
id: 'backup-app-11',
encryptionVersion: null,
packageVersion: '1.0.0',
version: '1.0.0',
type: backupdb.BACKUP_TYPE_APP,
dependsOn: [],
manifest: null,
@@ -341,26 +271,25 @@ describe('backups', function () {
describe('filesystem', function () {
var backupInfo1;
var backupConfig = {
var gBackupConfig = {
provider: 'filesystem',
backupFolder: path.join(os.tmpdir(), 'backups-test-filesystem'),
format: 'tgz',
retentionPolicy: { keepWithinSecs: 10000 }
format: 'tgz'
};
before(function (done) {
rimraf.sync(backupConfig.backupFolder);
rimraf.sync(gBackupConfig.backupFolder);
done();
});
after(function (done) {
rimraf.sync(backupConfig.backupFolder);
rimraf.sync(gBackupConfig.backupFolder);
done();
});
it('fails to set backup config for non-existing folder', function (done) {
settings.setBackupConfig(backupConfig, function (error) {
settings.setBackupConfig(gBackupConfig, function (error) {
expect(error).to.be.a(BoxError);
expect(error.reason).to.equal(BoxError.BAD_FIELD);
@@ -369,9 +298,9 @@ describe('backups', function () {
});
it('succeeds to set backup config', function (done) {
mkdirp.sync(backupConfig.backupFolder);
mkdirp.sync(gBackupConfig.backupFolder);
settings.setBackupConfig(backupConfig, function (error) {
settings.setBackupConfig(gBackupConfig, function (error) {
expect(error).to.be(null);
done();
@@ -384,8 +313,8 @@ describe('backups', function () {
createBackup(function (error, result) {
expect(error).to.be(null);
expect(fs.statSync(path.join(backupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
expect(fs.statSync(path.join(backupConfig.backupFolder, `${result.id}.tar.gz`)).nlink).to.be(2);
expect(fs.statSync(path.join(gBackupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
expect(fs.statSync(path.join(gBackupConfig.backupFolder, `${result.id}.tar.gz`)).nlink).to.be(2);
backupInfo1 = result;
@@ -399,9 +328,9 @@ describe('backups', function () {
createBackup(function (error, result) {
expect(error).to.be(null);
expect(fs.statSync(path.join(backupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
expect(fs.statSync(path.join(backupConfig.backupFolder, `${result.id}.tar.gz`)).nlink).to.be(2); // hard linked to new backup
expect(fs.statSync(path.join(backupConfig.backupFolder, `${backupInfo1.id}.tar.gz`)).nlink).to.be(1); // not hard linked anymore
expect(fs.statSync(path.join(gBackupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
expect(fs.statSync(path.join(gBackupConfig.backupFolder, `${result.id}.tar.gz`)).nlink).to.be(2); // hard linked to new backup
expect(fs.statSync(path.join(gBackupConfig.backupFolder, `${backupInfo1.id}.tar.gz`)).nlink).to.be(1); // not hard linked anymore
done();
});
+50 -39
View File
@@ -40,8 +40,7 @@ var USER_0 = {
twoFactorAuthenticationSecret: '',
role: 'user',
active: true,
source: '',
resetTokenCreationTime: Date.now()
source: ''
};
var USER_1 = {
@@ -59,8 +58,7 @@ var USER_1 = {
twoFactorAuthenticationSecret: '',
role: 'user',
active: true,
source: '',
resetTokenCreationTime: Date.now()
source: ''
};
var USER_2 = {
@@ -78,8 +76,7 @@ var USER_2 = {
twoFactorAuthenticationSecret: '',
role: 'user',
active: true,
source: '',
resetTokenCreationTime: Date.now()
source: ''
};
const DOMAIN_0 = {
@@ -418,9 +415,7 @@ describe('database', function () {
dataDir: null,
tags: [],
label: null,
taskId: null,
binds: {},
servicesConfig: {}
taskId: null
};
it('cannot delete referenced domain', function (done) {
@@ -891,9 +886,7 @@ describe('database', function () {
dataDir: null,
tags: [],
label: null,
taskId: null,
binds: {},
servicesConfig: {}
taskId: null
};
var APP_1 = {
@@ -924,9 +917,7 @@ describe('database', function () {
dataDir: null,
tags: [],
label: null,
taskId: null,
binds: {},
servicesConfig: {}
taskId: null
};
before(function (done) {
@@ -989,7 +980,7 @@ describe('database', function () {
appdb.get(APP_0.id, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an('object');
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime', 'resetTokenCreationTime'])).to.be.eql(APP_0);
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime'])).to.be.eql(APP_0);
done();
});
});
@@ -1029,7 +1020,7 @@ describe('database', function () {
appdb.get(APP_0.id, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an('object');
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime','resetTokenCreationTime'])).to.be.eql(APP_0);
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime'])).to.be.eql(APP_0);
done();
});
});
@@ -1039,7 +1030,7 @@ describe('database', function () {
appdb.getByHttpPort(APP_0.httpPort, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an('object');
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime','resetTokenCreationTime'])).to.be.eql(APP_0);
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime'])).to.be.eql(APP_0);
done();
});
});
@@ -1064,8 +1055,8 @@ describe('database', function () {
expect(error).to.be(null);
expect(result).to.be.an(Array);
expect(result.length).to.be(2);
expect(_.omit(result[0], ['creationTime', 'updateTime','ts', 'healthTime', 'resetTokenCreationTime'])).to.be.eql(APP_0);
expect(_.omit(result[1], ['creationTime', 'updateTime','ts', 'healthTime', 'resetTokenCreationTime'])).to.be.eql(APP_1);
expect(_.omit(result[0], ['creationTime', 'updateTime','ts', 'healthTime'])).to.be.eql(APP_0);
expect(_.omit(result[1], ['creationTime', 'updateTime','ts', 'healthTime'])).to.be.eql(APP_1);
done();
});
});
@@ -1324,8 +1315,7 @@ describe('database', function () {
it('add succeeds', function (done) {
var backup = {
id: 'backup-box',
encryptionVersion: 2,
packageVersion: '1.0.0',
version: '1.0.0',
type: backupdb.BACKUP_TYPE_BOX,
dependsOn: [ 'dep1' ],
manifest: null,
@@ -1341,8 +1331,7 @@ describe('database', function () {
it('get succeeds', function (done) {
backupdb.get('backup-box', function (error, result) {
expect(error).to.be(null);
expect(result.encryptionVersion).to.be(2);
expect(result.packageVersion).to.be('1.0.0');
expect(result.version).to.be('1.0.0');
expect(result.type).to.be(backupdb.BACKUP_TYPE_BOX);
expect(result.creationTime).to.be.a(Date);
expect(result.dependsOn).to.eql(['dep1']);
@@ -1367,8 +1356,7 @@ describe('database', function () {
expect(results.length).to.be(1);
expect(results[0].id).to.be('backup-box');
expect(results[0].encryptionVersion).to.be(2);
expect(results[0].packageVersion).to.be('1.0.0');
expect(results[0].version).to.be('1.0.0');
expect(results[0].dependsOn).to.eql(['dep1']);
expect(results[0].manifest).to.eql(null);
@@ -1393,8 +1381,7 @@ describe('database', function () {
it('add app succeeds', function (done) {
var backup = {
id: 'app_appid_123',
encryptionVersion: null,
packageVersion: '1.0.0',
version: '1.0.0',
type: backupdb.BACKUP_TYPE_APP,
dependsOn: [ ],
manifest: { foo: 'bar' },
@@ -1410,8 +1397,7 @@ describe('database', function () {
it('get succeeds', function (done) {
backupdb.get('app_appid_123', function (error, result) {
expect(error).to.be(null);
expect(result.encryptionVersion).to.be(null);
expect(result.packageVersion).to.be('1.0.0');
expect(result.version).to.be('1.0.0');
expect(result.type).to.be(backupdb.BACKUP_TYPE_APP);
expect(result.creationTime).to.be.a(Date);
expect(result.dependsOn).to.eql([]);
@@ -1427,8 +1413,7 @@ describe('database', function () {
expect(results.length).to.be(1);
expect(results[0].id).to.be('app_appid_123');
expect(results[0].encryptionVersion).to.be(null);
expect(results[0].packageVersion).to.be('1.0.0');
expect(results[0].version).to.be('1.0.0');
expect(results[0].dependsOn).to.eql([]);
expect(results[0].manifest).to.eql({ foo: 'bar' });
@@ -1789,6 +1774,7 @@ describe('database', function () {
before(function (done) {
async.series([
domaindb.add.bind(null, DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }),
maildb.add.bind(null, DOMAIN_0.domain, {})
], done);
});
@@ -1841,7 +1827,7 @@ describe('database', function () {
});
it('can set alias', function (done) {
mailboxdb.setAliasesForName('support', DOMAIN_0.domain, [ { name: 'support2', domain: DOMAIN_0.domain }, { name: 'help', domain: DOMAIN_0.domain } ], function (error) {
mailboxdb.setAliasesForName('support', DOMAIN_0.domain, [ 'support2', 'help' ], function (error) {
expect(error).to.be(null);
done();
});
@@ -1851,10 +1837,8 @@ describe('database', function () {
mailboxdb.getAliasesForName('support', DOMAIN_0.domain, function (error, results) {
expect(error).to.be(null);
expect(results.length).to.be(2);
expect(results[0].name).to.be('help');
expect(results[0].domain).to.be(DOMAIN_0.domain);
expect(results[1].name).to.be('support2');
expect(results[1].domain).to.be(DOMAIN_0.domain);
expect(results[0]).to.be('help');
expect(results[1]).to.be('support2');
done();
});
});
@@ -1863,8 +1847,18 @@ describe('database', function () {
mailboxdb.getAlias('support2', DOMAIN_0.domain, function (error, result) {
expect(error).to.be(null);
expect(result.name).to.be('support2');
expect(result.aliasName).to.be('support');
expect(result.aliasDomain).to.be(DOMAIN_0.domain);
expect(result.aliasTarget).to.be('support');
done();
});
});
it('can list aliases', function (done) {
mailboxdb.listAliases(DOMAIN_0.domain, 1, 10, function (error, results) {
expect(error).to.be(null);
expect(results.length).to.be(2);
expect(results[0].name).to.be('help');
expect(results[0].aliasTarget).to.be('support');
expect(results[1].name).to.be('support2');
done();
});
});
@@ -1954,6 +1948,23 @@ describe('database', function () {
database._clear(done);
});
it('cannot add non-existing domain', function (done) {
maildb.add(MAIL_DOMAIN_0.domain + 'nope', {}, function (error) {
expect(error).to.be.ok();
expect(error.reason).to.be(BoxError.NOT_FOUND);
done();
});
});
it('can add domain', function (done) {
maildb.add(MAIL_DOMAIN_0.domain, {}, function (error) {
expect(error).to.equal(null);
done();
});
});
it('can get all domains', function (done) {
maildb.list(function (error, result) {
expect(error).to.equal(null);
+1 -1
View File
@@ -11,7 +11,7 @@ var constants = require('../constants.js'),
exec = require('child_process').exec,
expect = require('expect.js');
const DOCKER = `docker -H tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT} `;
const DOCKER = `docker -H tcp://localhost:${constants.DOCKER_PROXY_PORT} `;
describe('Dockerproxy', function () {
var containerId;
+3 -4
View File
@@ -19,7 +19,6 @@ var appdb = require('../appdb.js'),
maildb = require('../maildb.js'),
mailboxdb = require('../mailboxdb.js'),
ldap = require('ldapjs'),
settings = require('../settings.js'),
users = require('../users.js');
const DOMAIN_0 = {
@@ -88,8 +87,8 @@ function setup(done) {
database.initialize.bind(null),
database._clear.bind(null),
ldapServer.start.bind(null),
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
maildb.add.bind(null, DOMAIN_0.domain, {}),
function (callback) {
users.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE, function (error, result) {
if (error) return callback(error);
@@ -108,7 +107,7 @@ function setup(done) {
});
},
(done) => mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, done),
(done) => mailboxdb.setAliasesForName(USER_0.username.toLowerCase(), DOMAIN_0.domain, [ { name: USER_0_ALIAS.toLocaleLowerCase(), domain: DOMAIN_0.domain} ], done),
(done) => mailboxdb.setAliasesForName(USER_0.username.toLowerCase(), DOMAIN_0.domain, [ USER_0_ALIAS.toLocaleLowerCase() ], done),
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
appdb.setAddonConfig.bind(null, APP_0.id, 'sendmail', [{ name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]),
appdb.setAddonConfig.bind(null, APP_0.id, 'recvmail', [{ name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]),
@@ -790,7 +789,7 @@ describe('Ldap', function () {
describe('search mailing list', function () {
before(function (done) {
mailboxdb.addList('devs', DOMAIN_0.domain, [ USER_0.username.toLowerCase() + '@' + DOMAIN_0.domain , USER_1.username.toLowerCase() + '@external.com' ], false /* membersOnly */, done);
mailboxdb.addList('devs', DOMAIN_0.domain, [ USER_0.username.toLowerCase() + '@' + DOMAIN_0.domain , USER_1.username.toLowerCase() + '@external.com' ], done);
});
it('get specific list', function (done) {
+1
View File
@@ -34,6 +34,7 @@ function setup(done) {
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
mail.addDomain.bind(null, DOMAIN_0.domain)
], done);
}
+1 -16
View File
@@ -67,17 +67,6 @@ describe('Certificates', function () {
var validCert2 = '-----BEGIN CERTIFICATE-----\nMIIBwjCCAWwCCQCZjm6jL50XfTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wHhcN\nMTYxMTA4MDgyMDE1WhcNMjAxMTA3MDgyMDE1WjBoMQswCQYDVQQGEwJERTEPMA0G\nA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05lYnVsb24x\nDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEAtKoyTPrf2DjKbnW7Xr1HbRvV+quHTcGmUq5anDI7G4w/\nabqDXGYyakHHlPyZxYp7FWQxCm83rHUuDT1LiLIBZQIDAQABMA0GCSqGSIb3DQEB\nCwUAA0EAVaD2Q6bF9hcUUBev5NyjaMdDYURuWfjuwWUkb8W50O2ed3O+MATKrDdS\nyVaBy8W02KJ4Y1ym4je/MF8nilPurA==\n-----END CERTIFICATE-----';
var validKey2 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBPQIBAAJBALSqMkz639g4ym51u169R20b1fqrh03BplKuWpwyOxuMP2m6g1xm\nMmpBx5T8mcWKexVkMQpvN6x1Lg09S4iyAWUCAwEAAQJBAJXu7YHPbjfuoalcUZzF\nbuKRCFtZQRf5z0Os6QvZ8A3iR0SzYJzx+c2ibp7WdifMXp3XaKm4tHSOfumrjUIq\nt10CIQDrs9Xo7bq0zuNjUV5IshNfaiYKZRfQciRVW2O8xBP9VwIhAMQ5CCEDZy+u\nsaF9RtmB0bjbe6XonBlAzoflfH/MAwWjAiEA50hL+ohr0MfCMM7DKaozgEj0kvan\n645VQLywnaX5x3kCIQDCwjinS9FnKmV0e/uOd6PJb0/S5IXLKt/TUpu33K5DMQIh\nAM9peu3B5t9pO59MmeUGZwI+bEJfEb+h03WTptBxS3pO\n-----END RSA PRIVATE KEY-----';
/*
Generate these with:
openssl ecparam -genkey -name prime256v1 -out server.key
openssl req -new -sha256 -key server.key -out server.csr
openssl req -x509 -sha256 -days 1460 -key server.key -in server.csr -out server.crt
*/
// *.foobar.com
var validCert4 = '-----BEGIN CERTIFICATE-----\nMIICDDCCAbOgAwIBAgIUduLaSQC6kh9LxVdua1EUBCgQOHYwCgYIKoZIzj0EAwIw\nXDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu\ndGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEVMBMGA1UEAwwMKi5mb29iYXIuY29tMB4X\nDTIwMDMyNTA0MTYxMloXDTI0MDMyNDA0MTYxMlowXDELMAkGA1UEBhMCQVUxEzAR\nBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5\nIEx0ZDEVMBMGA1UEAwwMKi5mb29iYXIuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0D\nAQcDQgAEmBum8MbyGXKuLP+NEOmR15XlemPEHR4b68A+B0Zjh/cuLQncAIwfmLT7\nutUOh3CivEKvZYkQIdd71xhCbVtbkqNTMFEwHQYDVR0OBBYEFCxEvAFsSFyAITNw\niBttbdsyEwO4MB8GA1UdIwQYMBaAFCxEvAFsSFyAITNwiBttbdsyEwO4MA8GA1Ud\nEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgd+rxp8xTXy7wsV45hiu1HQ2p\nwrEEPFmfPinVHwhDCiECIAEnIr5bEYUzSjujiHg7C2q3zh41XJhZWQie3VHLY/Kt\n-----END CERTIFICATE-----\n';
var validKey4 = '-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAXuQG4YDaQuwOCvWOZjkOvw/Y5V8Oum+rWnliMTsA5woAoGCCqGSM49\nAwEHoUQDQgAEmBum8MbyGXKuLP+NEOmR15XlemPEHR4b68A+B0Zjh/cuLQncAIwf\nmLT7utUOh3CivEKvZYkQIdd71xhCbVtbkg==\n-----END EC PRIVATE KEY-----\n';
// cp /etc/ssl/openssl.cnf /tmp/openssl.cnf
// echo -e "[SAN]\nsubjectAltName=DNS:amazing.com,DNS:*.amazing.com\n" >> /tmp/openssl.cnf
// openssl req -x509 -newkey rsa:2048 -keyout amazing.key -out amazing.crt -days 3650 -subj /CN=*.amazing.com -nodes -extensions SAN -config /tmp/openssl.cnf
@@ -134,10 +123,6 @@ describe('Certificates', function () {
expect(reverseProxy.validateCertificate('', amazingDomain, { cert: validCert3, key: validKey3 })).to.be(null);
expect(reverseProxy.validateCertificate('subdomain', amazingDomain, { cert: validCert3, key: validKey3 })).to.be(null);
});
it('allows valid cert with matching domain (subdomain) - ecdsa', function () {
expect(reverseProxy.validateCertificate('baz', foobarDomain, { cert: validCert4, key: validKey4 })).to.be(null);
});
});
describe('generateFallbackCertificiate - non-hyphenated', function () {
@@ -196,7 +181,7 @@ describe('Certificates', function () {
reverseProxy._getCertApi(DOMAIN_0, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('caas');
expect(options).to.eql({ email: 'webmaster@cloudron.io', 'performHttpAuthorization': false, 'prod': false, 'wildcard': false });
expect(options).to.eql({ email: 'support@cloudron.io', 'performHttpAuthorization': false, 'prod': false, 'wildcard': false });
done();
});
});
+28 -21
View File
@@ -13,6 +13,7 @@ var appdb = require('../appdb.js'),
database = require('../database.js'),
domains = require('../domains.js'),
expect = require('expect.js'),
mail = require('../mail.js'),
mailer = require('../mailer.js'),
nock = require('nock'),
paths = require('../paths.js'),
@@ -81,6 +82,7 @@ describe('updatechecker - box - manual (email)', function () {
cron.startJobs,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
mail.addDomain.bind(null, DOMAIN_0.domain),
users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settings.setBoxAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
settingsdb.set.bind(null, settings.CLOUDRON_TOKEN_KEY, 'atoken'),
@@ -94,10 +96,10 @@ describe('updatechecker - box - manual (email)', function () {
var scope = nock('http://localhost:4444')
.get('/api/v1/boxupdate')
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', automatic: false })
.query({ boxVersion: constants.VERSION, accessToken: 'atoken' })
.reply(204, { } );
updatechecker.checkBoxUpdates({ automatic: false }, function (error) {
updatechecker.checkBoxUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().box).to.be(null);
expect(scope.isDone()).to.be.ok();
@@ -111,10 +113,10 @@ describe('updatechecker - box - manual (email)', function () {
var scope = nock('http://localhost:4444')
.get('/api/v1/boxupdate')
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', automatic: false })
.query({ boxVersion: constants.VERSION, accessToken: 'atoken' })
.reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz', sourceTarballSigUrl: 'box.tar.gz.sig', boxVersionsUrl: 'box.versions', boxVersionsSigUrl: 'box.versions.sig' } );
updatechecker.checkBoxUpdates({ automatic: false }, function (error) {
updatechecker.checkBoxUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION);
expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz');
@@ -129,10 +131,10 @@ describe('updatechecker - box - manual (email)', function () {
var scope = nock('http://localhost:4444')
.get('/api/v1/boxupdate')
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', automatic: false })
.query({ boxVersion: constants.VERSION, accessToken: 'atoken' })
.reply(404, { version: '2.0.0-pre.0', changelog: [''], sourceTarballUrl: 'box-pre.tar.gz' } );
updatechecker.checkBoxUpdates({ automatic: false }, function (error) {
updatechecker.checkBoxUpdates(function (error) {
expect(error).to.be.ok();
expect(updatechecker.getUpdateInfo().box).to.be(null);
expect(scope.isDone()).to.be.ok();
@@ -152,6 +154,7 @@ describe('updatechecker - box - automatic (no email)', function () {
cron.startJobs,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
mail.addDomain.bind(null, DOMAIN_0.domain),
users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settingsdb.set.bind(null, settings.CLOUDRON_TOKEN_KEY, 'atoken'),
], done);
@@ -164,10 +167,10 @@ describe('updatechecker - box - automatic (no email)', function () {
var scope = nock('http://localhost:4444')
.get('/api/v1/boxupdate')
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', automatic: false })
.query({ boxVersion: constants.VERSION, accessToken: 'atoken' })
.reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz', sourceTarballSigUrl: 'box.tar.gz.sig', boxVersionsUrl: 'box.versions', boxVersionsSigUrl: 'box.versions.sig' } );
updatechecker.checkBoxUpdates({ automatic: false }, function (error) {
updatechecker.checkBoxUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION);
expect(scope.isDone()).to.be.ok();
@@ -187,6 +190,7 @@ describe('updatechecker - box - automatic free (email)', function () {
cron.startJobs,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
mail.addDomain.bind(null, DOMAIN_0.domain),
users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settingsdb.set.bind(null, settings.CLOUDRON_TOKEN_KEY, 'atoken'),
], done);
@@ -199,10 +203,10 @@ describe('updatechecker - box - automatic free (email)', function () {
var scope = nock('http://localhost:4444')
.get('/api/v1/boxupdate')
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', automatic: false })
.query({ boxVersion: constants.VERSION, accessToken: 'atoken' })
.reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz', sourceTarballSigUrl: 'box.tar.gz.sig', boxVersionsUrl: 'box.versions', boxVersionsSigUrl: 'box.versions.sig' } );
updatechecker.checkBoxUpdates({ automatic: false }, function (error) {
updatechecker.checkBoxUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION);
expect(scope.isDone()).to.be.ok();
@@ -250,6 +254,7 @@ describe('updatechecker - app - manual (email)', function () {
cron.startJobs,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
mail.addDomain.bind(null, DOMAIN_0.domain),
users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0),
settings.setAppAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
@@ -264,10 +269,10 @@ describe('updatechecker - app - manual (email)', function () {
var scope = nock('http://localhost:4444')
.get('/api/v1/appupdate')
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version, automatic: false })
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version })
.reply(204, { } );
updatechecker.checkAppUpdates({ automatic: false }, function (error) {
updatechecker.checkAppUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().apps).to.eql({});
expect(scope.isDone()).to.be.ok();
@@ -281,10 +286,10 @@ describe('updatechecker - app - manual (email)', function () {
var scope = nock('http://localhost:4444')
.get('/api/v1/appupdate')
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version, automatic: false })
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version })
.reply(500, { update: { manifest: { version: '1.0.0', changelog: '* some changes' } } } );
updatechecker.checkAppUpdates({ automatic: false }, function (error) {
updatechecker.checkAppUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().apps).to.eql({});
expect(scope.isDone()).to.be.ok();
@@ -298,10 +303,10 @@ describe('updatechecker - app - manual (email)', function () {
var scope = nock('http://localhost:4444')
.get('/api/v1/appupdate')
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version, automatic: false })
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version })
.reply(200, { manifest: { version: '2.0.0', changelog: '* some changes' } } );
updatechecker.checkAppUpdates({ automatic: false }, function (error) {
updatechecker.checkAppUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().apps).to.eql({ 'appid-0': { manifest: { version: '2.0.0', changelog: '* some changes' } } });
expect(scope.isDone()).to.be.ok();
@@ -313,7 +318,7 @@ describe('updatechecker - app - manual (email)', function () {
it('does not offer old version', function (done) {
nock.cleanAll();
updatechecker.checkAppUpdates({ automatic: false }, function (error) {
updatechecker.checkAppUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().apps).to.eql({ });
checkMails(0, done);
@@ -359,6 +364,7 @@ describe('updatechecker - app - automatic (no email)', function () {
cron.startJobs,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
mail.addDomain.bind(null, DOMAIN_0.domain),
users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0),
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
@@ -373,10 +379,10 @@ describe('updatechecker - app - automatic (no email)', function () {
var scope = nock('http://localhost:4444')
.get('/api/v1/appupdate')
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version, automatic: false })
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version })
.reply(200, { manifest: { version: '2.0.0', changelog: 'c' } } );
updatechecker.checkAppUpdates({ automatic: false }, function (error) {
updatechecker.checkAppUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().apps).to.eql({ 'appid-0': { manifest: { version: '2.0.0', changelog: 'c' } } });
expect(scope.isDone()).to.be.ok();
@@ -424,6 +430,7 @@ describe('updatechecker - app - automatic free (email)', function () {
cron.startJobs,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
mail.addDomain.bind(null, DOMAIN_0.domain),
users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0),
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
@@ -438,10 +445,10 @@ describe('updatechecker - app - automatic free (email)', function () {
var scope = nock('http://localhost:4444')
.get('/api/v1/appupdate')
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version, automatic: false })
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version })
.reply(200, { manifest: { version: '2.0.0', changelog: 'c' } } );
updatechecker.checkAppUpdates({ automatic: false }, function (error) {
updatechecker.checkAppUpdates(function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().apps).to.eql({ 'appid-0': { manifest: { version: '2.0.0', changelog: 'c' } } });
expect(scope.isDone()).to.be.ok();
+1
View File
@@ -74,6 +74,7 @@ function setup(done) {
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
mail.addDomain.bind(null, DOMAIN_0.domain),
], done);
}
+4 -6
View File
@@ -62,8 +62,7 @@ function resetAppUpdateInfo(appId) {
}
}
function checkAppUpdates(options, callback) {
assert.strictEqual(typeof options, 'object');
function checkAppUpdates(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Checking App Updates');
@@ -85,7 +84,7 @@ function checkAppUpdates(options, callback) {
if (app.appStoreId === '') return iteratorDone(); // appStoreId can be '' for dev apps
if (app.runState === apps.RSTATE_STOPPED) return iteratorDone(); // stopped apps won't run migration scripts and shouldn't be updated
appstore.getAppUpdate(app, options, function (error, updateInfo) {
appstore.getAppUpdate(app, function (error, updateInfo) {
if (error) {
debug('Error getting app update info for %s', app.id, error);
return iteratorDone(); // continue to next
@@ -137,15 +136,14 @@ function checkAppUpdates(options, callback) {
});
}
function checkBoxUpdates(options, callback) {
assert.strictEqual(typeof options, 'object');
function checkBoxUpdates(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Checking Box Updates');
gBoxUpdateInfo = null;
appstore.getBoxUpdate(options, function (error, updateInfo) {
appstore.getBoxUpdate(function (error, updateInfo) {
if (error || !updateInfo) return callback(error);
gBoxUpdateInfo = updateInfo;
+3 -6
View File
@@ -64,16 +64,13 @@ function gpgVerify(file, sig, callback) {
debug(`gpgVerify: ${cmd}`);
child_process.exec(cmd, { encoding: 'utf8' }, function (error, stdout, stderr) {
if (error) {
debug(`gpgVerify: command failed. error: ${error}\n stdout: ${stdout}\n stderr: ${stderr}`);
return callback(new BoxError(BoxError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not verified (command failed)`));
}
if (error) return callback(new BoxError(BoxError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not verified`));
if (stdout.indexOf('[GNUPG:] VALIDSIG 0EADB19CDDA23CD0FE71E3470A372F8703C493CC') !== -1) return callback();
if (stdout.indexOf('[GNUPG:] VALIDSIG 0EADB19CDDA23CD0FE71E3470A372F8703C493CC')) return callback();
debug(`gpgVerify: verification of ${sig} failed: ${stdout}\n${stderr}`);
return callback(new BoxError(BoxError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not verified (bad sig)`));
return callback(new BoxError(BoxError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not verified`));
});
}
+1 -1
View File
@@ -29,7 +29,7 @@ var assert = require('assert'),
mysql = require('mysql');
var USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'createdAt', 'modifiedAt', 'resetToken', 'displayName',
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime' ].join(',');
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role' ].join(',');
var APP_PASSWORD_FIELDS = [ 'id', 'name', 'userId', 'identifier', 'hashedPassword', 'creationTime' ].join(',');
+7 -7
View File
@@ -31,6 +31,7 @@ exports = module.exports = {
count: count,
AP_MAIL: 'mail',
AP_SFTP: 'sftp',
AP_WEBADMIN: 'webadmin',
ROLE_ADMIN: 'admin',
@@ -495,11 +496,10 @@ function resetPasswordByIdentifier(identifier, callback) {
getter(identifier.toLowerCase(), function (error, result) {
if (error) return callback(error);
let resetToken = hat(256), resetTokenCreationTime = new Date();
let resetToken = hat(256);
result.resetToken = resetToken;
result.resetTokenCreationTime = resetTokenCreationTime;
userdb.update(result.id, { resetToken, resetTokenCreationTime }, function (error) {
userdb.update(result.id, { resetToken }, function (error) {
if (error) return callback(error);
mailer.passwordReset(result);
@@ -584,13 +584,13 @@ function createInvite(user, callback) {
if (user.source) return callback(new BoxError(BoxError.CONFLICT, 'User is from an external directory'));
const resetToken = hat(256), resetTokenCreationTime = new Date();
let resetToken = hat(256);
user.resetToken = resetToken;
userdb.update(user.id, { resetToken, resetTokenCreationTime }, function (error) {
userdb.update(user.id, { resetToken }, function (error) {
if (error) return callback(error);
user.resetToken = resetToken;
callback(null, { resetToken, inviteLink: inviteLink(user) });
callback(null, { resetToken: user.resetToken, inviteLink: inviteLink(user) });
});
}