Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90c24cf356 | ||
|
|
54abada561 | ||
|
|
f1922660be | ||
|
|
795e3c57da | ||
|
|
3f201464a5 | ||
|
|
8ac0be6bb5 | ||
|
|
130805e7bd | ||
|
|
b8c7357fea | ||
|
|
819f8e338f | ||
|
|
9569e46ff8 | ||
|
|
b7baab2d0f | ||
|
|
e2d284797d | ||
|
|
a3ac343fe2 | ||
|
|
dadde96e41 | ||
|
|
99475c51e8 | ||
|
|
cc9b4e26b5 | ||
|
|
32f232d3c0 | ||
|
|
235047ad0b | ||
|
|
228f75de0b | ||
|
|
2f89e7e2b4 | ||
|
|
437f39deb3 | ||
|
|
59582f16c4 | ||
|
|
af9e3e38ce | ||
|
|
d992702b87 | ||
|
|
6a9fe1128f | ||
|
|
573da29a4d | ||
|
|
00cff1a728 | ||
|
|
9bdeff0a39 | ||
|
|
a1f263c048 | ||
|
|
346eac389c | ||
|
|
f52c16b209 | ||
|
|
4faf880aa4 | ||
|
|
f417a49b34 | ||
|
|
66fd713d12 | ||
|
|
2e7630f97e | ||
|
|
3f10524532 | ||
|
|
51f9826918 | ||
|
|
f5bb76333b | ||
|
|
4947faa5ca | ||
|
|
101dc3a93c | ||
|
|
bd3ee0fa24 | ||
|
|
2c52668a74 | ||
|
|
03edd8c96b | ||
|
|
37dfa41e01 | ||
|
|
ea8a3d798e | ||
|
|
1df94fd84d | ||
|
|
5af957dc9c | ||
|
|
21073c627e | ||
|
|
66cdba9c1a | ||
|
|
56d3b38ce6 | ||
|
|
15d0275045 | ||
|
|
991c1a0137 | ||
|
|
7d549dbbd5 | ||
|
|
e27c5583bb | ||
|
|
650c49637f | ||
|
|
eb5dcf1c3e | ||
|
|
ed2b61b709 | ||
|
|
41466a3018 | ||
|
|
2e130ef99d | ||
|
|
a96fb39a82 | ||
|
|
c9923c8d4b | ||
|
|
74b0ff338b | ||
|
|
dcaccc2d7a | ||
|
|
d60714e4e6 | ||
|
|
d513d5d887 | ||
|
|
386566fd4b | ||
|
|
3357ca76fe | ||
|
|
a183ce13ee | ||
|
|
e9d0ed8e1e | ||
|
|
66f66fd14f | ||
|
|
b49d30b477 | ||
|
|
73d83ec57e | ||
|
|
efb39fb24b | ||
|
|
73623f2e92 | ||
|
|
fbcc4cfa50 | ||
|
|
474a3548e0 | ||
|
|
2cdf68379b | ||
|
|
cc8509f8eb | ||
|
|
a520c1b1cb | ||
|
|
75fc2cbcfb | ||
|
|
b8bb69f730 | ||
|
|
b46d3e74d6 | ||
|
|
77a1613107 | ||
|
|
62fab7b09f | ||
|
|
5d87352b28 | ||
|
|
ff60f5a381 | ||
|
|
7f666d9369 | ||
|
|
442f16dbd0 | ||
|
|
2dcab77ed1 | ||
|
|
13be04a169 | ||
|
|
e3767c3a54 | ||
|
|
ce957c8dd5 | ||
|
|
0606b2994c | ||
|
|
33acccbaaa | ||
|
|
1e097abe86 | ||
|
|
e51705c41d | ||
|
|
7eafa661fe | ||
|
|
2fe323e587 | ||
|
|
4e608d04dc | ||
|
|
531d314e25 | ||
|
|
1ab23d2902 | ||
|
|
b3496e1354 | ||
|
|
2efa0aaca4 | ||
|
|
ef9aeb0772 | ||
|
|
924a0136eb | ||
|
|
c382fc375e | ||
|
|
2544acddfa | ||
|
|
58072892d6 | ||
|
|
85a897c78c | ||
|
|
6adf5772d8 | ||
|
|
f98e3b1960 | ||
|
|
671a967e35 |
69
CHANGES
69
CHANGES
@@ -1867,3 +1867,72 @@
|
||||
* 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
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -48,18 +48,8 @@ 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.
|
||||
|
||||
## Documentation
|
||||
## Support
|
||||
|
||||
* [Documentation](https://cloudron.io/documentation/)
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
17
migrations/20200417235435-mailboxes-add-membersOnly.js
Normal file
17
migrations/20200417235435-mailboxes-add-membersOnly.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
|
||||
28
migrations/20200420013715-mailboxes-add-aliasDomain.js
Normal file
28
migrations/20200420013715-mailboxes-add-aliasDomain.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'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);
|
||||
};
|
||||
15
migrations/20200427044800-apps-add-servicesConfigJson.js
Normal file
15
migrations/20200427044800-apps-add-servicesConfigJson.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
15
migrations/20200430035310-apps-add-bindsJson.js
Normal file
15
migrations/20200430035310-apps-add-bindsJson.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
35
migrations/20200512172301-settings-backup-encryption.js
Normal file
35
migrations/20200512172301-settings-backup-encryption.js
Normal file
@@ -0,0 +1,35 @@
|
||||
'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();
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
24
migrations/20200514045746-backups-add-encryptionVersion.js
Normal file
24
migrations/20200514045746-backups-add-encryptionVersion.js
Normal file
@@ -0,0 +1,24 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
'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();
|
||||
};
|
||||
@@ -86,6 +86,7 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
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),
|
||||
@@ -120,7 +121,8 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
|
||||
CREATE TABLE IF NOT EXISTS backups(
|
||||
id VARCHAR(128) NOT NULL,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
version VARCHAR(128) NOT NULL, /* app version or box version */
|
||||
packageVersion VARCHAR(128) NOT NULL, /* app version or box version */
|
||||
encryptionVersion INTEGER, /* when null, unencrypted backup */
|
||||
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
|
||||
dependsOn TEXT, /* comma separate list of objects this backup depends on */
|
||||
state VARCHAR(16) NOT NULL,
|
||||
@@ -177,12 +179,15 @@ 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 */
|
||||
aliasTarget VARCHAR(128), /* the target name type is an alias */
|
||||
aliasName VARCHAR(128), /* the target name type is an alias */
|
||||
aliasDomain VARCHAR(128), /* the target domain */
|
||||
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(
|
||||
|
||||
92
package-lock.json
generated
92
package-lock.json
generated
@@ -338,7 +338,7 @@
|
||||
},
|
||||
"amdefine": {
|
||||
"version": "1.0.1",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
|
||||
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -411,7 +411,7 @@
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||
},
|
||||
"assertion-error": {
|
||||
@@ -483,7 +483,7 @@
|
||||
},
|
||||
"backoff": {
|
||||
"version": "2.5.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
|
||||
"integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
|
||||
"requires": {
|
||||
"precond": "0.2"
|
||||
@@ -621,7 +621,7 @@
|
||||
},
|
||||
"buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
|
||||
},
|
||||
"buffer-fill": {
|
||||
@@ -636,7 +636,7 @@
|
||||
},
|
||||
"bunyan": {
|
||||
"version": "1.8.12",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
|
||||
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
|
||||
"requires": {
|
||||
"dtrace-provider": "~0.8",
|
||||
@@ -762,7 +762,7 @@
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -807,7 +807,7 @@
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"concat-stream": {
|
||||
@@ -992,7 +992,7 @@
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"cron": {
|
||||
@@ -1034,7 +1034,7 @@
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0"
|
||||
@@ -1109,7 +1109,7 @@
|
||||
},
|
||||
"decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
||||
},
|
||||
"deep-eql": {
|
||||
@@ -1376,7 +1376,7 @@
|
||||
},
|
||||
"ent": {
|
||||
"version": "2.2.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
|
||||
"integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0="
|
||||
},
|
||||
"error-ex": {
|
||||
@@ -1480,7 +1480,7 @@
|
||||
},
|
||||
"expect.js": {
|
||||
"version": "0.3.1",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz",
|
||||
"integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -2267,7 +2267,7 @@
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
@@ -2303,7 +2303,7 @@
|
||||
},
|
||||
"is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -2385,12 +2385,12 @@
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
||||
},
|
||||
"isstream": {
|
||||
@@ -2526,7 +2526,7 @@
|
||||
},
|
||||
"ldap-filter": {
|
||||
"version": "0.2.2",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
|
||||
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
|
||||
"requires": {
|
||||
"assert-plus": "0.1.5"
|
||||
@@ -2534,7 +2534,7 @@
|
||||
"dependencies": {
|
||||
"assert-plus": {
|
||||
"version": "0.1.5",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
|
||||
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
|
||||
}
|
||||
}
|
||||
@@ -2733,19 +2733,19 @@
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
@@ -2876,9 +2876,9 @@
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
|
||||
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
|
||||
"version": "2.25.3",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.25.3.tgz",
|
||||
"integrity": "sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg=="
|
||||
},
|
||||
"moment-timezone": {
|
||||
"version": "0.5.27",
|
||||
@@ -2943,7 +2943,7 @@
|
||||
},
|
||||
"mv": {
|
||||
"version": "2.1.1",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
|
||||
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
@@ -2954,7 +2954,7 @@
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "6.0.4",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
|
||||
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
@@ -2967,13 +2967,13 @@
|
||||
},
|
||||
"ncp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
|
||||
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
|
||||
"optional": true
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.4.5",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
|
||||
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
@@ -3261,7 +3261,7 @@
|
||||
},
|
||||
"nopt": {
|
||||
"version": "3.0.6",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
|
||||
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -3310,7 +3310,7 @@
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -3470,7 +3470,7 @@
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "2.2.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
|
||||
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -3494,7 +3494,7 @@
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"path-key": {
|
||||
@@ -3584,7 +3584,7 @@
|
||||
},
|
||||
"precond": {
|
||||
"version": "0.2.3",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
|
||||
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
|
||||
},
|
||||
"pretty-bytes": {
|
||||
@@ -3652,7 +3652,7 @@
|
||||
},
|
||||
"pseudomap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -3920,7 +3920,7 @@
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
|
||||
},
|
||||
"require-main-filename": {
|
||||
@@ -4194,7 +4194,7 @@
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
||||
},
|
||||
"setprototypeof": {
|
||||
@@ -4254,7 +4254,7 @@
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.2",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"smtp-connection": {
|
||||
@@ -4348,7 +4348,7 @@
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"sqlstring": {
|
||||
@@ -4515,7 +4515,7 @@
|
||||
},
|
||||
"stubs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
|
||||
"integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls="
|
||||
},
|
||||
"superagent": {
|
||||
@@ -4812,7 +4812,7 @@
|
||||
},
|
||||
"typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
},
|
||||
"uid-safe": {
|
||||
@@ -4884,7 +4884,7 @@
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"utile": {
|
||||
@@ -4939,7 +4939,7 @@
|
||||
},
|
||||
"vasync": {
|
||||
"version": "1.6.4",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
|
||||
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
|
||||
"requires": {
|
||||
"verror": "1.6.0"
|
||||
@@ -4947,7 +4947,7 @@
|
||||
"dependencies": {
|
||||
"verror": {
|
||||
"version": "1.6.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
|
||||
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
|
||||
"requires": {
|
||||
"extsprintf": "1.2.0"
|
||||
@@ -4957,7 +4957,7 @@
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0",
|
||||
@@ -4980,7 +4980,7 @@
|
||||
},
|
||||
"which-module": {
|
||||
"version": "2.0.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
|
||||
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
|
||||
},
|
||||
"wide-align": {
|
||||
@@ -5067,7 +5067,7 @@
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"write-file-atomic": {
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"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",
|
||||
|
||||
@@ -11,9 +11,8 @@ if [[ ${EUID} -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly USER=yellowtent
|
||||
readonly BOX_SRC_DIR=/home/${USER}/box
|
||||
readonly BASE_DATA_DIR=/home/${USER}
|
||||
readonly user=yellowtent
|
||||
readonly box_src_dir=/home/${user}/box
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -24,6 +23,8 @@ 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
|
||||
@@ -118,22 +119,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"
|
||||
|
||||
@@ -20,6 +20,11 @@ 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
|
||||
@@ -56,6 +61,7 @@ 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
|
||||
@@ -170,9 +176,11 @@ 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 ! 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 ! 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
|
||||
echo "DB migration failed"
|
||||
exit 1
|
||||
fi
|
||||
@@ -191,6 +199,9 @@ 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
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
/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
|
||||
@@ -17,6 +18,7 @@
|
||||
missingok
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
# this truncates the original log file and not the rotated one
|
||||
copytruncate
|
||||
}
|
||||
|
||||
|
||||
@@ -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 --max_old_space_size=150 /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 /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
|
||||
|
||||
387
src/addons.js
387
src/addons.js
@@ -7,6 +7,9 @@ exports = module.exports = {
|
||||
getServiceLogs: getServiceLogs,
|
||||
restartService: restartService,
|
||||
|
||||
startAppServices,
|
||||
stopAppServices,
|
||||
|
||||
startServices: startServices,
|
||||
updateServiceConfig: updateServiceConfig,
|
||||
|
||||
@@ -20,7 +23,7 @@ exports = module.exports = {
|
||||
getMountsSync: getMountsSync,
|
||||
getContainerNamesSync: getContainerNamesSync,
|
||||
|
||||
getServiceDetails: getServiceDetails,
|
||||
getContainerDetails: getContainerDetails,
|
||||
|
||||
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
|
||||
SERVICE_STATUS_ACTIVE: 'active',
|
||||
@@ -62,7 +65,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 KNOWN_ADDONS = {
|
||||
var ADDONS = {
|
||||
turn: {
|
||||
setup: setupTurn,
|
||||
teardown: teardownTurn,
|
||||
@@ -146,10 +149,18 @@ var KNOWN_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,
|
||||
}
|
||||
};
|
||||
|
||||
const KNOWN_SERVICES = {
|
||||
// services are actual containers that are running. addons are the concepts requested by app
|
||||
const SERVICES = {
|
||||
turn: {
|
||||
status: statusTurn,
|
||||
restart: restartContainer.bind(null, 'turn'),
|
||||
@@ -202,6 +213,16 @@ const KNOWN_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');
|
||||
|
||||
@@ -249,25 +270,22 @@ function rebuildService(serviceName, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
function restartContainer(serviceName, callback) {
|
||||
assert.strictEqual(typeof serviceName, 'string');
|
||||
function restartContainer(name, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.stopContainer(serviceName, function (error) {
|
||||
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); });
|
||||
}
|
||||
if (error) return 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);
|
||||
});
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function getServiceDetails(containerName, tokenEnvName, callback) {
|
||||
function getContainerDetails(containerName, tokenEnvName, callback) {
|
||||
assert.strictEqual(typeof containerName, 'string');
|
||||
assert.strictEqual(typeof tokenEnvName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -290,20 +308,20 @@ function getServiceDetails(containerName, tokenEnvName, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function containerStatus(addonName, addonTokenName, callback) {
|
||||
assert.strictEqual(typeof addonName, 'string');
|
||||
assert.strictEqual(typeof addonTokenName, 'string');
|
||||
function containerStatus(containerName, tokenEnvName, callback) {
|
||||
assert.strictEqual(typeof containerName, 'string');
|
||||
assert.strictEqual(typeof tokenEnvName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getServiceDetails(addonName, addonTokenName, function (error, addonDetails) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
|
||||
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 });
|
||||
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 ${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}` });
|
||||
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}` });
|
||||
|
||||
docker.memoryUsage(addonName, function (error, result) {
|
||||
docker.memoryUsage(containerName, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var tmp = {
|
||||
@@ -321,19 +339,59 @@ function containerStatus(addonName, addonTokenName, callback) {
|
||||
function getServices(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let services = Object.keys(KNOWN_SERVICES);
|
||||
let services = Object.keys(SERVICES);
|
||||
|
||||
callback(null, 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);
|
||||
});
|
||||
}
|
||||
|
||||
function getService(serviceName, callback) {
|
||||
assert.strictEqual(typeof serviceName, 'string');
|
||||
function getServicesConfig(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
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));
|
||||
}
|
||||
|
||||
var tmp = {
|
||||
name: serviceName,
|
||||
name: name,
|
||||
status: null,
|
||||
memoryUsed: 0,
|
||||
memoryPercent: 0,
|
||||
@@ -345,60 +403,76 @@ function getService(serviceName, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
containerStatusFunc(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
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;
|
||||
}
|
||||
tmp.status = result.status;
|
||||
tmp.memoryUsed = result.memoryUsed;
|
||||
tmp.memoryPercent = result.memoryPercent;
|
||||
tmp.error = result.error || null;
|
||||
|
||||
KNOWN_SERVICES[serviceName].status(function (error, result) {
|
||||
getServicesConfig(id, function (error, service, servicesConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tmp.status = result.status;
|
||||
tmp.memoryUsed = result.memoryUsed;
|
||||
tmp.memoryPercent = result.memoryPercent;
|
||||
tmp.error = result.error || null;
|
||||
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;
|
||||
}
|
||||
|
||||
callback(null, tmp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function configureService(serviceName, data, callback) {
|
||||
assert.strictEqual(typeof serviceName, 'string');
|
||||
function configureService(id, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
const [name, instance ] = id.split(':');
|
||||
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
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) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!platformConfig[serviceName]) platformConfig[serviceName] = {};
|
||||
if (!servicesConfig[name]) servicesConfig[name] = {};
|
||||
|
||||
// if not specified we clear the entry and use defaults
|
||||
if (!data.memory || !data.memorySwap) {
|
||||
delete platformConfig[serviceName];
|
||||
delete servicesConfig[name];
|
||||
} else {
|
||||
platformConfig[serviceName].memory = data.memory;
|
||||
platformConfig[serviceName].memorySwap = data.memorySwap;
|
||||
servicesConfig[name].memory = data.memory;
|
||||
servicesConfig[name].memorySwap = data.memorySwap;
|
||||
}
|
||||
|
||||
settings.setPlatformConfig(platformConfig, function (error) {
|
||||
if (error) return callback(error);
|
||||
if (instance) {
|
||||
appdb.update(instance, { servicesConfig }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
updateAppServiceConfig(name, instance, servicesConfig, callback);
|
||||
});
|
||||
} else {
|
||||
settings.setPlatformConfig(servicesConfig, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getServiceLogs(serviceName, options, callback) {
|
||||
assert.strictEqual(typeof serviceName, 'string');
|
||||
function getServiceLogs(id, options, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert(options && typeof options === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -406,9 +480,15 @@ function getServiceLogs(serviceName, options, callback) {
|
||||
assert.strictEqual(typeof options.format, 'string');
|
||||
assert.strictEqual(typeof options.follow, 'boolean');
|
||||
|
||||
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
const [name, instance ] = id.split(':');
|
||||
|
||||
debug(`Getting logs for ${serviceName}`);
|
||||
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}`);
|
||||
|
||||
var lines = options.lines,
|
||||
format = options.format || 'json',
|
||||
@@ -417,11 +497,11 @@ function getServiceLogs(serviceName, options, callback) {
|
||||
let cmd, args = [];
|
||||
|
||||
// docker and unbound use journald
|
||||
if (serviceName === 'docker' || serviceName === 'unbound') {
|
||||
if (name === 'docker' || name === 'unbound') {
|
||||
cmd = 'journalctl';
|
||||
|
||||
args.push('--lines=' + (lines === -1 ? 'all' : lines));
|
||||
args.push(`--unit=${serviceName}`);
|
||||
args.push(`--unit=${name}`);
|
||||
args.push('--no-pager');
|
||||
args.push('--output=short-iso');
|
||||
|
||||
@@ -431,7 +511,8 @@ function getServiceLogs(serviceName, 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
|
||||
args.push(path.join(paths.LOG_DIR, serviceName, 'app.log'));
|
||||
const containerName = APP_SERVICES[name] ? `${name}-${instance}` : name;
|
||||
args.push(path.join(paths.LOG_DIR, containerName, 'app.log'));
|
||||
}
|
||||
|
||||
var cp = spawn(cmd, args);
|
||||
@@ -450,7 +531,7 @@ function getServiceLogs(serviceName, options, callback) {
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: timestamp * 1000,
|
||||
message: message,
|
||||
source: serviceName
|
||||
source: name
|
||||
}) + '\n';
|
||||
});
|
||||
|
||||
@@ -461,23 +542,67 @@ function getServiceLogs(serviceName, options, callback) {
|
||||
callback(null, transformStream);
|
||||
}
|
||||
|
||||
function restartService(serviceName, callback) {
|
||||
assert.strictEqual(typeof serviceName, 'string');
|
||||
function restartService(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
const [name, instance ] = id.split(':');
|
||||
|
||||
KNOWN_SERVICES[serviceName].restart(callback);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
function waitForService(containerName, tokenEnvName, 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) {
|
||||
assert.strictEqual(typeof containerName, 'string');
|
||||
assert.strictEqual(typeof tokenEnvName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`Waiting for ${containerName}`);
|
||||
|
||||
getServiceDetails(containerName, tokenEnvName, function (error, result) {
|
||||
getContainerDetails(containerName, tokenEnvName, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
|
||||
@@ -501,11 +626,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 KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||
|
||||
debugApp(app, 'Setting up addon %s with options %j', addon, addons[addon]);
|
||||
|
||||
KNOWN_ADDONS[addon].setup(app, addons[addon], iteratorCallback);
|
||||
ADDONS[addon].setup(app, addons[addon], iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -519,11 +644,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 KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||
|
||||
debugApp(app, 'Tearing down addon %s with options %j', addon, addons[addon]);
|
||||
|
||||
KNOWN_ADDONS[addon].teardown(app, addons[addon], iteratorCallback);
|
||||
ADDONS[addon].teardown(app, addons[addon], iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -539,9 +664,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 KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||
|
||||
KNOWN_ADDONS[addon].backup(app, addons[addon], iteratorCallback);
|
||||
ADDONS[addon].backup(app, addons[addon], iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -557,9 +682,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 KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||
|
||||
KNOWN_ADDONS[addon].clear(app, addons[addon], iteratorCallback);
|
||||
ADDONS[addon].clear(app, addons[addon], iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -575,9 +700,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 KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||
|
||||
KNOWN_ADDONS[addon].restore(app, addons[addon], iteratorCallback);
|
||||
ADDONS[addon].restore(app, addons[addon], iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -586,12 +711,12 @@ function importAppDatabase(app, addon, callback) {
|
||||
assert.strictEqual(typeof addon, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!(addon in KNOWN_ADDONS)) return callback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||
if (!(addon in ADDONS)) return callback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||
|
||||
async.series([
|
||||
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])
|
||||
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])
|
||||
], callback);
|
||||
}
|
||||
|
||||
@@ -633,7 +758,7 @@ function updateServiceConfig(platformConfig, callback) {
|
||||
memory = containerConfig.memory;
|
||||
memorySwap = containerConfig.memorySwap;
|
||||
} else {
|
||||
memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
|
||||
memory = SERVICES[serviceName].defaultMemoryLimit;
|
||||
memorySwap = memory * 2;
|
||||
}
|
||||
|
||||
@@ -642,6 +767,28 @@ 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');
|
||||
@@ -744,8 +891,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, `${app.id}-localstorage`),
|
||||
docker.createVolume.bind(null, app, `${app.id}-localstorage`, volumeDataDir)
|
||||
docker.removeVolume.bind(null, `${app.id}-localstorage`),
|
||||
docker.createVolume.bind(null, `${app.id}-localstorage`, volumeDataDir, { fqdn: app.fqdn, appId: app.id })
|
||||
], callback);
|
||||
}
|
||||
|
||||
@@ -756,7 +903,7 @@ function clearLocalStorage(app, options, callback) {
|
||||
|
||||
debugApp(app, 'clearLocalStorage');
|
||||
|
||||
docker.clearVolume(app, `${app.id}-localstorage`, { removeDirectory: false }, callback);
|
||||
docker.clearVolume(`${app.id}-localstorage`, { removeDirectory: false }, callback);
|
||||
}
|
||||
|
||||
function teardownLocalStorage(app, options, callback) {
|
||||
@@ -767,8 +914,8 @@ function teardownLocalStorage(app, options, callback) {
|
||||
debugApp(app, 'teardownLocalStorage');
|
||||
|
||||
async.series([
|
||||
docker.clearVolume.bind(null, app, `${app.id}-localstorage`, { removeDirectory: true }),
|
||||
docker.removeVolume.bind(null, app, `${app.id}-localstorage`)
|
||||
docker.clearVolume.bind(null, `${app.id}-localstorage`, { removeDirectory: true }),
|
||||
docker.removeVolume.bind(null, `${app.id}-localstorage`)
|
||||
], callback);
|
||||
}
|
||||
|
||||
@@ -1004,7 +1151,7 @@ function startMysql(existingInfra, callback) {
|
||||
shell.exec('startMysql', cmd, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
waitForService('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
|
||||
waitForContainer('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
|
||||
if (error) return callback(error);
|
||||
if (!upgrading) return callback(null);
|
||||
|
||||
@@ -1033,7 +1180,7 @@ function setupMySql(app, options, callback) {
|
||||
password: error ? hat(4 * 48) : existingPassword // see box#362 for password length
|
||||
};
|
||||
|
||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
getContainerDetails('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) {
|
||||
@@ -1072,7 +1219,7 @@ function clearMySql(app, options, callback) {
|
||||
|
||||
const database = mysqlDatabaseName(app.id);
|
||||
|
||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
getContainerDetails('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) {
|
||||
@@ -1092,7 +1239,7 @@ function teardownMySql(app, options, callback) {
|
||||
const database = mysqlDatabaseName(app.id);
|
||||
const username = database;
|
||||
|
||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
getContainerDetails('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) {
|
||||
@@ -1140,7 +1287,7 @@ function backupMySql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Backing up mysql');
|
||||
|
||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
getContainerDetails('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}`;
|
||||
@@ -1159,7 +1306,7 @@ function restoreMySql(app, options, callback) {
|
||||
|
||||
callback = once(callback); // protect from multiple returns with streams
|
||||
|
||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var input = fs.createReadStream(dumpPath('mysql', app.id));
|
||||
@@ -1220,7 +1367,7 @@ function startPostgresql(existingInfra, callback) {
|
||||
shell.exec('startPostgresql', cmd, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
waitForService('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
|
||||
waitForContainer('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
|
||||
if (error) return callback(error);
|
||||
if (!upgrading) return callback(null);
|
||||
|
||||
@@ -1248,7 +1395,7 @@ function setupPostgreSql(app, options, callback) {
|
||||
password: error ? hat(4 * 128) : existingPassword
|
||||
};
|
||||
|
||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
getContainerDetails('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) {
|
||||
@@ -1282,7 +1429,7 @@ function clearPostgreSql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Clearing postgresql');
|
||||
|
||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
getContainerDetails('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) {
|
||||
@@ -1301,7 +1448,7 @@ function teardownPostgreSql(app, options, callback) {
|
||||
|
||||
const { database, username } = postgreSqlNames(app.id);
|
||||
|
||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
getContainerDetails('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) {
|
||||
@@ -1322,7 +1469,7 @@ function backupPostgreSql(app, options, callback) {
|
||||
|
||||
const { database } = postgreSqlNames(app.id);
|
||||
|
||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
getContainerDetails('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}`;
|
||||
@@ -1341,7 +1488,7 @@ function restorePostgreSql(app, options, callback) {
|
||||
|
||||
callback = once(callback); // protect from multiple returns with streams
|
||||
|
||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var input = fs.createReadStream(dumpPath('postgresql', app.id));
|
||||
@@ -1434,7 +1581,7 @@ function startMongodb(existingInfra, callback) {
|
||||
shell.exec('startMongodb', cmd, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
waitForService('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
|
||||
waitForContainer('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
|
||||
if (error) return callback(error);
|
||||
if (!upgrading) return callback(null);
|
||||
|
||||
@@ -1461,7 +1608,7 @@ function setupMongoDb(app, options, callback) {
|
||||
oplog: !!options.oplog
|
||||
};
|
||||
|
||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
|
||||
@@ -1497,7 +1644,7 @@ function clearMongodb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Clearing mongodb');
|
||||
|
||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
getContainerDetails('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) {
|
||||
@@ -1516,7 +1663,7 @@ function teardownMongoDb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Tearing down mongodb');
|
||||
|
||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
getContainerDetails('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) {
|
||||
@@ -1535,7 +1682,7 @@ function backupMongoDb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Backing up mongodb');
|
||||
|
||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
getContainerDetails('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}`;
|
||||
@@ -1552,7 +1699,7 @@ function restoreMongoDb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'restoreMongoDb');
|
||||
|
||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
|
||||
@@ -1608,15 +1755,7 @@ function setupRedis(app, options, callback) {
|
||||
const redisServiceToken = hat(4 * 48);
|
||||
|
||||
// Compute redis memory limit based on app's memory limit (this is arbitrary)
|
||||
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 memoryLimit = app.servicesConfig['redis'] ? app.servicesConfig['redis'].memoryLimit : APP_SERVICES['redis'].defaultMemoryLimit;
|
||||
|
||||
const tag = infra.images.redis.tag;
|
||||
const label = app.fqdn;
|
||||
@@ -1660,7 +1799,7 @@ function setupRedis(app, options, callback) {
|
||||
});
|
||||
},
|
||||
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
|
||||
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
|
||||
waitForContainer.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
|
||||
], function (error) {
|
||||
if (error) debug('Error setting up redis: ', error);
|
||||
callback(error);
|
||||
@@ -1675,7 +1814,7 @@ function clearRedis(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Clearing redis');
|
||||
|
||||
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||
getContainerDetails('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) {
|
||||
@@ -1714,7 +1853,7 @@ function backupRedis(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Backing up redis');
|
||||
|
||||
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||
getContainerDetails('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}`;
|
||||
@@ -1731,7 +1870,7 @@ function restoreRedis(app, options, callback) {
|
||||
|
||||
callback = once(callback); // protect from multiple returns with streams
|
||||
|
||||
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||
getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let input;
|
||||
@@ -1869,3 +2008,13 @@ 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);
|
||||
}
|
||||
|
||||
12
src/appdb.js
12
src/appdb.js
@@ -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.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.bindsJson',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
|
||||
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
|
||||
@@ -94,6 +94,14 @@ 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;
|
||||
@@ -427,7 +435,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') {
|
||||
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'binds') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
|
||||
|
||||
62
src/apps.js
62
src/apps.js
@@ -20,6 +20,7 @@ exports = module.exports = {
|
||||
setTags: setTags,
|
||||
setMemoryLimit: setMemoryLimit,
|
||||
setCpuShares: setCpuShares,
|
||||
setBinds: setBinds,
|
||||
setAutomaticBackup: setAutomaticBackup,
|
||||
setAutomaticUpdate: setAutomaticUpdate,
|
||||
setReverseProxyConfig: setReverseProxyConfig,
|
||||
@@ -332,6 +333,20 @@ function validateEnv(env) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateBinds(binds) {
|
||||
for (let name of Object.keys(binds)) {
|
||||
// just have friendly characters under /media
|
||||
if (!/^[-0-9a-zA-Z_@$=#.%+]+$/.test(name)) return new BoxError(BoxError.BAD_FIELD, `Invalid bind name: ${name}`);
|
||||
|
||||
const bind = binds[name];
|
||||
|
||||
if (!bind.hostPath.startsWith('/mnt') && !bind.hostPath.startsWith('/media')) return new BoxError(BoxError.BAD_FIELD, 'hostPath must be in /mnt or /media');
|
||||
if (path.normalize(bind.hostPath) !== bind.hostPath) return new BoxError(BoxError.BAD_FIELD, 'hostPath is not normalized');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateDataDir(dataDir) {
|
||||
if (dataDir === null) return null;
|
||||
|
||||
@@ -404,7 +419,7 @@ function removeInternalFields(app) {
|
||||
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares',
|
||||
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
|
||||
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir');
|
||||
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'binds');
|
||||
}
|
||||
|
||||
// non-admins can only see these
|
||||
@@ -605,6 +620,8 @@ function downloadManifest(appStoreId, manifest, callback) {
|
||||
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text)));
|
||||
|
||||
if (!result.body.manifest || typeof result.body.manifest !== 'object') return callback(new BoxError(BoxError.NOT_FOUND, util.format('Missing manifest. Failed to get app info from store.', result.statusCode, result.text)));
|
||||
|
||||
callback(null, parts[0], result.body.manifest);
|
||||
});
|
||||
}
|
||||
@@ -761,6 +778,8 @@ function install(data, auditSource, callback) {
|
||||
error = validateEnv(env);
|
||||
if (error) return callback(error);
|
||||
|
||||
if (settings.isDemo() && constants.DEMO_BLACKLISTED_APPS.includes(appStoreId)) return callback(new BoxError(BoxError.BAD_FIELD, 'This app is blacklisted in the demo'));
|
||||
|
||||
const mailboxName = hasMailAddon(manifest) ? mailboxNameForLocation(location, manifest) : null;
|
||||
const mailboxDomain = hasMailAddon(manifest) ? domain : null;
|
||||
const appId = uuid.v4();
|
||||
@@ -968,6 +987,32 @@ function setCpuShares(app, cpuShares, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setBinds(app, binds, auditSource, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(binds && typeof binds === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const appId = app.id;
|
||||
let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateBinds(binds);
|
||||
if (error) return callback(error);
|
||||
|
||||
const task = {
|
||||
args: {},
|
||||
values: { binds }
|
||||
};
|
||||
addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, binds, taskId: result.taskId });
|
||||
|
||||
callback(null, { taskId: result.taskId });
|
||||
});
|
||||
}
|
||||
|
||||
function setEnvironment(app, env, auditSource, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof env, 'object');
|
||||
@@ -1435,7 +1480,8 @@ function restore(app, backupId, auditSource, callback) {
|
||||
func(function (error, backupInfo) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest'));
|
||||
if (!backupInfo.manifest) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest'));
|
||||
if (backupInfo.encryptionVersion === 1) return callback(new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and has to be restored using the CLI tool'));
|
||||
|
||||
// re-validate because this new box version may not accept old configs
|
||||
error = checkManifestConstraints(backupInfo.manifest);
|
||||
@@ -1495,6 +1541,11 @@ function importApp(app, data, auditSource, callback) {
|
||||
testBackupConfig(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (backupConfig && 'password' in backupConfig) {
|
||||
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password);
|
||||
delete backupConfig.password;
|
||||
}
|
||||
|
||||
const restoreConfig = { backupId, backupFormat, backupConfig };
|
||||
|
||||
const task = {
|
||||
@@ -1553,7 +1604,8 @@ function clone(app, data, user, auditSource, callback) {
|
||||
backups.get(backupId, function (error, backupInfo) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config'));
|
||||
if (!backupInfo.manifest) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config'));
|
||||
if (backupInfo.encryptionVersion === 1) return callback(new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and cannot be cloned'));
|
||||
|
||||
const manifest = backupInfo.manifest, appStoreId = app.appStoreId;
|
||||
|
||||
@@ -1777,6 +1829,10 @@ function exec(app, options, callback) {
|
||||
|
||||
function canAutoupdateApp(app, newManifest) {
|
||||
if (!app.enableAutomaticUpdate) return false;
|
||||
|
||||
// for invalid subscriptions the appstore does not return a dockerImage
|
||||
if (!newManifest.dockerImage) return false;
|
||||
|
||||
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return false; // major changes are blocking
|
||||
|
||||
if (app.runState === exports.RSTATE_STOPPED) return false; // stopped apps won't run migration scripts and shouldn't be updated
|
||||
|
||||
@@ -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));
|
||||
if (result.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
|
||||
|
||||
callback(null);
|
||||
@@ -340,7 +340,8 @@ function sendAliveStatus(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getBoxUpdate(callback) {
|
||||
function getBoxUpdate(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
@@ -348,7 +349,13 @@ function getBoxUpdate(callback) {
|
||||
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
|
||||
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION }).timeout(10 * 1000).end(function (error, result) {
|
||||
const query = {
|
||||
accessToken: token,
|
||||
boxVersion: constants.VERSION,
|
||||
automatic: options.automatic
|
||||
};
|
||||
|
||||
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
@@ -374,16 +381,24 @@ function getBoxUpdate(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAppUpdate(app, callback) {
|
||||
function getAppUpdate(app, options, 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({ accessToken: token, boxVersion: constants.VERSION, appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
|
||||
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
@@ -415,7 +430,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: ${error.message}`));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${result.statusCode} ${error.message}`));
|
||||
|
||||
// cloudronId, token, licenseKey
|
||||
if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
|
||||
@@ -471,7 +486,7 @@ function registerWithLicense(license, domain, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (token) return callback(new BoxError(BoxError.CONFLICT));
|
||||
if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'));
|
||||
|
||||
const provider = settings.provider();
|
||||
const version = constants.VERSION;
|
||||
@@ -491,7 +506,7 @@ function registerWithLoginCredentials(options, callback) {
|
||||
}
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (token) return callback(new BoxError(BoxError.CONFLICT));
|
||||
if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'));
|
||||
|
||||
maybeSignup(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -899,7 +899,10 @@ function start(app, args, progressCallback, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 20, message: 'Starting container' }),
|
||||
progressCallback.bind(null, { percent: 10, message: 'Starting app services' }),
|
||||
addons.startAppServices.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 35, 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
|
||||
@@ -927,6 +930,9 @@ 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) {
|
||||
|
||||
@@ -6,7 +6,7 @@ var assert = require('assert'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs' ];
|
||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'packageVersion', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion' ];
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
@@ -106,7 +106,8 @@ function get(id, callback) {
|
||||
function add(id, data, callback) {
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof data.version, 'string');
|
||||
assert(data.encryptionVersion === null || typeof data.encryptionVersion === 'number');
|
||||
assert.strictEqual(typeof data.packageVersion, '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');
|
||||
@@ -116,8 +117,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, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[ id, data.version, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ],
|
||||
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 ],
|
||||
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));
|
||||
|
||||
456
src/backups.js
456
src/backups.js
@@ -32,12 +32,13 @@ exports = module.exports = {
|
||||
|
||||
configureCollectd: configureCollectd,
|
||||
|
||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
|
||||
generateEncryptionKeysSync: generateEncryptionKeysSync,
|
||||
|
||||
// for testing
|
||||
_getBackupFilePath: getBackupFilePath,
|
||||
_restoreFsMetadata: restoreFsMetadata,
|
||||
_saveFsMetadata: saveFsMetadata
|
||||
_saveFsMetadata: saveFsMetadata,
|
||||
_applyBackupRetentionPolicy: applyBackupRetentionPolicy
|
||||
};
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
@@ -57,6 +58,7 @@ 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'),
|
||||
@@ -69,6 +71,7 @@ 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');
|
||||
|
||||
@@ -94,19 +97,30 @@ 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 (newConfig.key === exports.SECRET_PLACEHOLDER) newConfig.key = currentConfig.key;
|
||||
if ('password' in newConfig) {
|
||||
if (newConfig.password === constants.SECRET_PLACEHOLDER) {
|
||||
delete newConfig.password;
|
||||
}
|
||||
newConfig.encryption = currentConfig.encryption || null;
|
||||
} else {
|
||||
newConfig.encryption = null;
|
||||
}
|
||||
if (newConfig.provider === currentConfig.provider) api(newConfig.provider).injectPrivateFields(newConfig, currentConfig);
|
||||
}
|
||||
|
||||
function removePrivateFields(backupConfig) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
if (backupConfig.key) backupConfig.key = exports.SECRET_PLACEHOLDER;
|
||||
if (backupConfig.encryption) {
|
||||
delete backupConfig.encryption;
|
||||
backupConfig.password = constants.SECRET_PLACEHOLDER;
|
||||
}
|
||||
return api(backupConfig.provider).removePrivateFields(backupConfig);
|
||||
}
|
||||
|
||||
@@ -120,12 +134,25 @@ 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: 'interval' }));
|
||||
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' }));
|
||||
|
||||
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');
|
||||
@@ -136,6 +163,18 @@ 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);
|
||||
@@ -179,21 +218,23 @@ function getBackupFilePath(backupConfig, backupId, format) {
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
if (format === 'tgz') {
|
||||
const fileType = backupConfig.key ? '.tar.gz.enc' : '.tar.gz';
|
||||
const fileType = backupConfig.encryption ? '.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, key) {
|
||||
function encryptFilePath(filePath, encryption) {
|
||||
assert.strictEqual(typeof filePath, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof encryption, 'object');
|
||||
|
||||
var encryptedParts = filePath.split('/').map(function (part) {
|
||||
const cipher = crypto.createCipher('aes-256-cbc', key);
|
||||
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);
|
||||
let crypt = cipher.update(part);
|
||||
crypt = Buffer.concat([ crypt, cipher.final() ]);
|
||||
crypt = Buffer.concat([ iv, crypt, cipher.final() ]);
|
||||
|
||||
return crypt.toString('base64') // ensures path is valid
|
||||
.replace(/\//g, '-') // replace '/' of base64 since it conflicts with path separator
|
||||
@@ -203,9 +244,9 @@ function encryptFilePath(filePath, key) {
|
||||
return encryptedParts.join('/');
|
||||
}
|
||||
|
||||
function decryptFilePath(filePath, key) {
|
||||
function decryptFilePath(filePath, encryption) {
|
||||
assert.strictEqual(typeof filePath, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof encryption, 'object');
|
||||
|
||||
let decryptedParts = [];
|
||||
for (let part of filePath.split('/')) {
|
||||
@@ -213,61 +254,172 @@ function decryptFilePath(filePath, key) {
|
||||
part = part.replace(/-/g, '/'); // replace with '/'
|
||||
|
||||
try {
|
||||
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'));
|
||||
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);
|
||||
} catch (error) {
|
||||
debug(`Error decrypting file ${filePath} part ${part}:`, error);
|
||||
return null;
|
||||
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}`) };
|
||||
}
|
||||
}
|
||||
|
||||
return decryptedParts.join('/');
|
||||
return { result: decryptedParts.join('/') };
|
||||
}
|
||||
|
||||
function createReadStream(sourceFile, key) {
|
||||
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) {
|
||||
assert.strictEqual(typeof sourceFile, 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof encryption, 'object');
|
||||
|
||||
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.', error);
|
||||
ps.emit('error', new BoxError(BoxError.FS_ERROR, error.message));
|
||||
debug(`createReadStream: read stream error at ${sourceFile}`, error);
|
||||
ps.emit('error', new BoxError(BoxError.FS_ERROR, `Error reading ${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));
|
||||
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}`));
|
||||
});
|
||||
return stream.pipe(encrypt).pipe(ps);
|
||||
|
||||
return stream.pipe(encryptStream).pipe(ps);
|
||||
} else {
|
||||
return stream.pipe(ps);
|
||||
}
|
||||
}
|
||||
|
||||
function createWriteStream(destFile, key) {
|
||||
function createWriteStream(destFile, encryption) {
|
||||
assert.strictEqual(typeof destFile, 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof encryption, 'object');
|
||||
|
||||
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.', error);
|
||||
ps.emit('error', new BoxError(BoxError.FS_ERROR, error.message));
|
||||
debug(`createWriteStream: write stream error ${destFile}`, error);
|
||||
ps.emit('error', new BoxError(BoxError.FS_ERROR, `Write error ${destFile}: ${error.message}`));
|
||||
});
|
||||
|
||||
if (key !== null) {
|
||||
var decrypt = crypto.createDecipher('aes-256-cbc', key);
|
||||
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);
|
||||
decrypt.on('error', function (error) {
|
||||
debug('createWriteStream: decrypt stream error.', error);
|
||||
ps.emit('error', new BoxError(BoxError.CRYPTO_ERROR, error.message));
|
||||
debug(`createWriteStream: decrypt stream error ${destFile}`, error);
|
||||
ps.emit('error', new BoxError(BoxError.CRYPTO_ERROR, `Decryption error at ${destFile}: ${error.message}`));
|
||||
});
|
||||
|
||||
ps.pipe(decrypt).pipe(stream);
|
||||
} else {
|
||||
ps.pipe(stream);
|
||||
@@ -276,9 +428,9 @@ function createWriteStream(destFile, key) {
|
||||
return ps;
|
||||
}
|
||||
|
||||
function tarPack(dataLayout, key, callback) {
|
||||
function tarPack(dataLayout, encryption, callback) {
|
||||
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof encryption, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var pack = tar.pack('/', {
|
||||
@@ -299,7 +451,7 @@ function tarPack(dataLayout, key, callback) {
|
||||
});
|
||||
|
||||
var gzip = zlib.createGzip({});
|
||||
var ps = progressStream({ time: 10000 }); // emit 'pgoress' every 10 seconds
|
||||
var ps = progressStream({ time: 10000 }); // emit 'progress' every 10 seconds
|
||||
|
||||
pack.on('error', function (error) {
|
||||
debug('tarPack: tar stream error.', error);
|
||||
@@ -311,18 +463,19 @@ function tarPack(dataLayout, key, callback) {
|
||||
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
|
||||
if (key !== null) {
|
||||
var encrypt = crypto.createCipher('aes-256-cbc', key);
|
||||
encrypt.on('error', function (error) {
|
||||
if (encryption) {
|
||||
const encryptStream = new EncryptStream(encryption);
|
||||
encryptStream.on('error', function (error) {
|
||||
debug('tarPack: encrypt stream error.', error);
|
||||
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
pack.pipe(gzip).pipe(encrypt).pipe(ps);
|
||||
|
||||
pack.pipe(gzip).pipe(encryptStream).pipe(ps);
|
||||
} else {
|
||||
pack.pipe(gzip).pipe(ps);
|
||||
}
|
||||
|
||||
callback(null, ps);
|
||||
return callback(null, ps);
|
||||
}
|
||||
|
||||
function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
|
||||
@@ -338,7 +491,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.key ? encryptFilePath(task.path, backupConfig.key) : task.path;
|
||||
const destPath = task.path && backupConfig.encryption ? encryptFilePath(task.path, backupConfig.encryption) : task.path;
|
||||
const backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), destPath);
|
||||
|
||||
if (task.operation === 'removedir') {
|
||||
@@ -359,7 +512,7 @@ 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.key || null);
|
||||
var stream = createReadStream(dataLayout.toLocalPath('./' + task.path), backupConfig.encryption);
|
||||
stream.on('error', function (error) {
|
||||
debug(`read stream error for ${task.path}: ${error.message}`);
|
||||
retryCallback();
|
||||
@@ -460,7 +613,7 @@ 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.key || null, function (error, tarStream) {
|
||||
tarPack(dataLayout, backupConfig.encryption, function (error, tarStream) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
tarStream.on('progress', function (progress) {
|
||||
@@ -483,10 +636,10 @@ function upload(backupId, format, dataLayoutString, progressCallback, callback)
|
||||
});
|
||||
}
|
||||
|
||||
function tarExtract(inStream, dataLayout, key, callback) {
|
||||
function tarExtract(inStream, dataLayout, encryption, callback) {
|
||||
assert.strictEqual(typeof inStream, 'object');
|
||||
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof encryption, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var gunzip = zlib.createGunzip({});
|
||||
@@ -498,7 +651,10 @@ function tarExtract(inStream, dataLayout, key, callback) {
|
||||
}
|
||||
});
|
||||
|
||||
const emitError = once((error) => ps.emit('error', error));
|
||||
const emitError = once((error) => {
|
||||
inStream.destroy();
|
||||
ps.emit('error', error);
|
||||
});
|
||||
|
||||
inStream.on('error', function (error) {
|
||||
debug('tarExtract: input stream error.', error);
|
||||
@@ -521,8 +677,8 @@ function tarExtract(inStream, dataLayout, key, callback) {
|
||||
ps.emit('done');
|
||||
});
|
||||
|
||||
if (key !== null) {
|
||||
var decrypt = crypto.createDecipher('aes-256-cbc', key);
|
||||
if (encryption) {
|
||||
let decrypt = new DecryptStream(encryption);
|
||||
decrypt.on('error', function (error) {
|
||||
debug('tarExtract: decrypt stream error.', error);
|
||||
emitError(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
|
||||
@@ -573,9 +729,10 @@ function downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback,
|
||||
|
||||
function downloadFile(entry, done) {
|
||||
let relativePath = path.relative(backupFilePath, entry.fullPath);
|
||||
if (backupConfig.key) {
|
||||
relativePath = decryptFilePath(relativePath, backupConfig.key);
|
||||
if (!relativePath) return done(new BoxError(BoxError.BAD_STATE, 'Unable to decrypt file'));
|
||||
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;
|
||||
}
|
||||
const destFilePath = dataLayout.toLocalPath('./' + relativePath);
|
||||
|
||||
@@ -583,31 +740,35 @@ 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) return closeAndRetry(error);
|
||||
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);
|
||||
|
||||
sourceStream.on('error', closeAndRetry);
|
||||
destStream.on('error', closeAndRetry); // already emits BoxError
|
||||
|
||||
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
|
||||
|
||||
sourceStream.pipe(destStream, { end: true }).on('finish', closeAndRetry);
|
||||
sourceStream.pipe(destStream, { end: true }).on('done', closeAndRetry);
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
@@ -638,7 +799,7 @@ 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.key || null, function (error, ps) {
|
||||
tarExtract(sourceStream, dataLayout, backupConfig.encryption, function (error, ps) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
ps.on('progress', function (progress) {
|
||||
@@ -817,7 +978,15 @@ function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, call
|
||||
|
||||
debug(`Rotating box backup to id ${backupId}`);
|
||||
|
||||
backupdb.add(backupId, { version: constants.VERSION, type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
|
||||
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) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format));
|
||||
@@ -855,9 +1024,10 @@ function backupBoxWithAppBackupIds(appBackupIds, tag, progressCallback, callback
|
||||
}
|
||||
|
||||
function canBackupApp(app) {
|
||||
// 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) ||
|
||||
// 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 ||
|
||||
app.installationState === apps.ISTATE_PENDING_CONFIGURE ||
|
||||
app.installationState === apps.ISTATE_PENDING_BACKUP || // called from apptask
|
||||
app.installationState === apps.ISTATE_PENDING_UPDATE; // called from apptask
|
||||
@@ -898,7 +1068,16 @@ function rotateAppBackup(backupConfig, app, tag, options, progressCallback, call
|
||||
|
||||
debug(`Rotating app backup of ${app.id} to id ${backupId}`);
|
||||
|
||||
backupdb.add(backupId, { version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], manifest: manifest, format: format }, function (error) {
|
||||
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) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format));
|
||||
@@ -1078,9 +1257,57 @@ function ensureBackup(auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function cleanupBackup(backupConfig, backup, 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) {
|
||||
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);
|
||||
@@ -1105,55 +1332,60 @@ function cleanupBackup(backupConfig, backup, 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', function (detail) { debug(`cleanupBackup: ${detail}`); });
|
||||
events.on('progress', (message) => progressCallback({ message: `${backup.id}: ${message}` }));
|
||||
events.on('done', done);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupAppBackups(backupConfig, referencedAppBackups, callback) {
|
||||
function cleanupAppBackups(backupConfig, referencedAppBackupIds, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert(Array.isArray(referencedAppBackups));
|
||||
assert(Array.isArray(referencedAppBackupIds));
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const now = new Date();
|
||||
let removedAppBackups = [];
|
||||
let removedAppBackupIds = [];
|
||||
|
||||
// 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 (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();
|
||||
if (appBackup.keepReason) return iteratorDone();
|
||||
|
||||
debug('cleanupAppBackups: removing %s', appBackup.id);
|
||||
progressCallback({ message: `Removing app backup ${appBackup.id}`});
|
||||
|
||||
removedAppBackups.push(appBackup.id);
|
||||
cleanupBackup(backupConfig, appBackup, iteratorDone);
|
||||
removedAppBackupIds.push(appBackup.id);
|
||||
cleanupBackup(backupConfig, appBackup, progressCallback, iteratorDone);
|
||||
}, function () {
|
||||
debug('cleanupAppBackups: done');
|
||||
|
||||
callback(null, removedAppBackups);
|
||||
callback(null, removedAppBackupIds);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cleanupBoxBackups(backupConfig, auditSource, callback) {
|
||||
function cleanupBoxBackups(backupConfig, progressCallback, auditSource, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const now = new Date();
|
||||
let referencedAppBackups = [], removedBoxBackups = [];
|
||||
let referencedAppBackupIds = [], removedBoxBackupIds = [];
|
||||
|
||||
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_BOX, 1, 1000, function (error, boxBackups) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (boxBackups.length === 0) return callback(null, { removedBoxBackups, referencedAppBackups });
|
||||
if (boxBackups.length === 0) return callback(null, { removedBoxBackupIds, referencedAppBackupIds });
|
||||
|
||||
// search for the first valid backup
|
||||
var i;
|
||||
@@ -1164,28 +1396,28 @@ function cleanupBoxBackups(backupConfig, auditSource, callback) {
|
||||
// keep the first valid backup
|
||||
if (i !== boxBackups.length) {
|
||||
debug('cleanupBoxBackups: preserving box backup %s (%j)', boxBackups[i].id, boxBackups[i].dependsOn);
|
||||
referencedAppBackups = boxBackups[i].dependsOn;
|
||||
referencedAppBackupIds = 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) {
|
||||
// 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);
|
||||
if (boxBackup.keepReason) {
|
||||
referencedAppBackupIds = referencedAppBackupIds.concat(boxBackup.dependsOn);
|
||||
return iteratorNext();
|
||||
}
|
||||
|
||||
debug('cleanupBoxBackups: removing %s', boxBackup.id);
|
||||
progressCallback({ message: `Removing box backup ${boxBackup.id}`});
|
||||
|
||||
removedBoxBackups.push(boxBackup.id);
|
||||
cleanupBackup(backupConfig, boxBackup, iteratorNext);
|
||||
removedBoxBackupIds.push(boxBackup.id);
|
||||
cleanupBackup(backupConfig, boxBackup, progressCallback, iteratorNext);
|
||||
}, function () {
|
||||
debug('cleanupBoxBackups: done');
|
||||
|
||||
callback(null, { removedBoxBackups, referencedAppBackups });
|
||||
callback(null, { removedBoxBackupIds, referencedAppBackupIds });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1247,19 +1479,19 @@ function cleanup(auditSource, progressCallback, callback) {
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (backupConfig.retentionSecs < 0) {
|
||||
if (backupConfig.retentionPolicy.keepWithinSecs < 0) {
|
||||
debug('cleanup: keeping all backups');
|
||||
return callback(null, {});
|
||||
}
|
||||
|
||||
progressCallback({ percent: 10, message: 'Cleaning box backups' });
|
||||
|
||||
cleanupBoxBackups(backupConfig, auditSource, function (error, result) {
|
||||
cleanupBoxBackups(backupConfig, progressCallback, auditSource, function (error, { removedBoxBackupIds, referencedAppBackupIds }) {
|
||||
if (error) return callback(error);
|
||||
|
||||
progressCallback({ percent: 40, message: 'Cleaning app backups' });
|
||||
|
||||
cleanupAppBackups(backupConfig, result.referencedAppBackups, function (error, removedAppBackups) {
|
||||
cleanupAppBackups(backupConfig, referencedAppBackupIds, progressCallback, function (error, removedAppBackupIds) {
|
||||
if (error) return callback(error);
|
||||
|
||||
progressCallback({ percent: 90, message: 'Cleaning snapshots' });
|
||||
@@ -1267,7 +1499,7 @@ function cleanup(auditSource, progressCallback, callback) {
|
||||
cleanupSnapshots(backupConfig, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, { removedBoxBackups: result.removedBoxBackups, removedAppBackups: removedAppBackups });
|
||||
callback(null, { removedBoxBackupIds, removedAppBackupIds });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 genrsa 4096');
|
||||
var key = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves
|
||||
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));
|
||||
|
||||
|
||||
@@ -37,10 +37,11 @@ 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),
|
||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8), // also used in dashboard client.js
|
||||
|
||||
CLOUDRON: CLOUDRON,
|
||||
TEST: TEST,
|
||||
@@ -49,6 +50,6 @@ exports = module.exports = {
|
||||
|
||||
FOOTER: '© 2020 [Cloudron](https://cloudron.io) [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() : '4.2.0-test'
|
||||
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '5.1.1-test'
|
||||
};
|
||||
|
||||
|
||||
@@ -80,14 +80,14 @@ function startJobs(callback) {
|
||||
});
|
||||
|
||||
gJobs.boxUpdateCheckerJob = new CronJob({
|
||||
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
|
||||
onTick: () => updateChecker.checkBoxUpdates(NOOP_CALLBACK),
|
||||
cronTime: '00 ' + randomMinute + ' 23 * * *', // once an day
|
||||
onTick: () => updateChecker.checkBoxUpdates({ automatic: true }, NOOP_CALLBACK),
|
||||
start: true
|
||||
});
|
||||
|
||||
gJobs.appUpdateChecker = new CronJob({
|
||||
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
|
||||
onTick: () => updateChecker.checkAppUpdates(NOOP_CALLBACK),
|
||||
cronTime: '00 ' + randomMinute + ' 22 * * *', // once an day
|
||||
onTick: () => updateChecker.checkAppUpdates({ automatic: true }, NOOP_CALLBACK),
|
||||
start: true
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ 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'),
|
||||
@@ -31,7 +32,7 @@ function getFqdn(location, domain) {
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
|
||||
// do not return the 'key'. in caas, this is private
|
||||
delete domainObject.fallbackCertificate.key;
|
||||
@@ -40,7 +41,7 @@ function removePrivateFields(domainObject) {
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function upsert(domainObject, location, type, values, callback) {
|
||||
|
||||
@@ -13,6 +13,7 @@ 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'),
|
||||
@@ -25,12 +26,12 @@ var assert = require('assert'),
|
||||
var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4';
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function translateRequestError(result, callback) {
|
||||
@@ -39,9 +40,14 @@ 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) && result.body.errors.length > 0) {
|
||||
let error = result.body.errors[0];
|
||||
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
|
||||
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}`;
|
||||
}
|
||||
return callback(new BoxError(BoxError.ACCESS_DENIED, message));
|
||||
}
|
||||
|
||||
@@ -284,7 +290,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 ('email' in dnsConfig && typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
|
||||
if (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';
|
||||
|
||||
@@ -13,6 +13,7 @@ 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'),
|
||||
@@ -28,12 +29,12 @@ function formatError(response) {
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function getInternal(dnsConfig, zoneName, name, type, callback) {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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'),
|
||||
@@ -26,12 +27,12 @@ function formatError(response) {
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function upsert(domainObject, location, type, values, callback) {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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'),
|
||||
@@ -21,12 +22,12 @@ var assert = require('assert'),
|
||||
_ = require('underscore');
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.credentials.private_key = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.credentials.private_key = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.credentials.private_key === domains.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
|
||||
if (newConfig.credentials.private_key === constants.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
|
||||
}
|
||||
|
||||
function getDnsCredentials(dnsConfig) {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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'),
|
||||
@@ -32,12 +33,12 @@ function formatError(response) {
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.apiSecret = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.apiSecret = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.apiSecret === domains.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
|
||||
if (newConfig.apiSecret === constants.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
|
||||
}
|
||||
|
||||
function upsert(domainObject, location, type, values, callback) {
|
||||
|
||||
@@ -21,13 +21,13 @@ var assert = require('assert'),
|
||||
util = require('util');
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
// in-place removal of tokens and api keys with domains.SECRET_PLACEHOLDER
|
||||
// in-place removal of tokens and api keys with constants.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 domains.SECRET_PLACEHOLDER
|
||||
// in-place injection of tokens and api keys which came in with constants.SECRET_PLACEHOLDER
|
||||
}
|
||||
|
||||
function upsert(domainObject, location, type, values, callback) {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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'),
|
||||
@@ -27,12 +28,12 @@ function formatError(response) {
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function getZoneId(dnsConfig, zoneName, callback) {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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'),
|
||||
@@ -25,12 +26,12 @@ var assert = require('assert'),
|
||||
const ENDPOINT = 'https://api.namecheap.com/xml.response';
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function getQuery(dnsConfig, callback) {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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'),
|
||||
@@ -27,12 +28,12 @@ function formatError(response) {
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function addRecord(dnsConfig, zoneName, name, type, values, callback) {
|
||||
@@ -54,6 +55,10 @@ 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];
|
||||
}
|
||||
@@ -91,6 +96,10 @@ 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];
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ 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'),
|
||||
@@ -21,12 +22,12 @@ var assert = require('assert'),
|
||||
_ = require('underscore');
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.secretAccessKey = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.secretAccessKey = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.secretAccessKey === domains.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
|
||||
if (newConfig.secretAccessKey === constants.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
|
||||
}
|
||||
|
||||
function getDnsCredentials(dnsConfig) {
|
||||
|
||||
@@ -6,8 +6,6 @@ exports = module.exports = {
|
||||
injectPrivateFields: injectPrivateFields,
|
||||
removePrivateFields: removePrivateFields,
|
||||
|
||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
|
||||
|
||||
ping: ping,
|
||||
|
||||
info: info,
|
||||
@@ -73,13 +71,13 @@ function testRegistryConfig(auth, callback) {
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.password === exports.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
|
||||
if (newConfig.password === constants.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
|
||||
}
|
||||
|
||||
function removePrivateFields(registryConfig) {
|
||||
assert.strictEqual(typeof registryConfig, 'object');
|
||||
|
||||
if (registryConfig.password) registryConfig.password = exports.SECRET_PLACEHOLDER;
|
||||
if (registryConfig.password) registryConfig.password = constants.SECRET_PLACEHOLDER;
|
||||
|
||||
return registryConfig;
|
||||
}
|
||||
@@ -188,6 +186,19 @@ 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');
|
||||
@@ -277,6 +288,7 @@ 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: {
|
||||
@@ -299,7 +311,8 @@ 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' ]
|
||||
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
|
||||
CapDrop: [ 'NET_RAW' ] // https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
|
||||
},
|
||||
NetworkingConfig: {
|
||||
EndpointsConfig: {
|
||||
@@ -313,7 +326,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
var capabilities = manifest.capabilities || [];
|
||||
if (capabilities.includes('net_admin')) {
|
||||
containerOptions.HostConfig.CapAdd = [
|
||||
'NET_ADMIN'
|
||||
'NET_ADMIN', 'NET_RAW'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -573,10 +586,10 @@ function memoryUsage(containerId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function createVolume(app, name, volumeDataDir, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
function createVolume(name, volumeDataDir, labels, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof volumeDataDir, 'string');
|
||||
assert.strictEqual(typeof labels, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const volumeOptions = {
|
||||
@@ -587,10 +600,7 @@ function createVolume(app, name, volumeDataDir, callback) {
|
||||
device: volumeDataDir,
|
||||
o: 'bind'
|
||||
},
|
||||
Labels: {
|
||||
'fqdn': app.fqdn,
|
||||
'appId': app.id
|
||||
},
|
||||
Labels: labels
|
||||
};
|
||||
|
||||
// requires sudo because the path can be outside appsdata
|
||||
@@ -605,8 +615,7 @@ function createVolume(app, name, volumeDataDir, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function clearVolume(app, name, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
function clearVolume(name, options, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -626,14 +635,13 @@ function clearVolume(app, name, options, callback) {
|
||||
}
|
||||
|
||||
// this only removes the volume and not the data
|
||||
function removeVolume(app, name, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
function removeVolume(name, callback) {
|
||||
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 of ${app.id} ${error.message}`));
|
||||
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume: ${error.message}`));
|
||||
|
||||
callback();
|
||||
});
|
||||
|
||||
@@ -28,9 +28,7 @@ module.exports = exports = {
|
||||
|
||||
checkDnsRecords: checkDnsRecords,
|
||||
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
|
||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
|
||||
prepareDashboardDomain: prepareDashboardDomain
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
|
||||
@@ -9,19 +9,19 @@ exports = module.exports = {
|
||||
'version': '48.17.0',
|
||||
|
||||
'baseImages': [
|
||||
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
|
||||
{ repo: 'cloudron/base', tag: 'cloudron/base:2.0.0@sha256:f9fea80513aa7c92fe2e7bf3978b54c8ac5222f47a9a32a7f8833edf0eb5a4f4' }
|
||||
],
|
||||
|
||||
// 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.0.1@sha256:d7e8b3a88e30986a459a4da9742fbd50a1d150f07408942b91e279c41e6af059' },
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.2.0@sha256:440c8a9ca4d2958d51a375359f8158ef702b83395aa9ac4f450c51825ec09239' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.1.0@sha256:f2cda21bd15c21bbf44432df412525369ef831a2d53860b5c5b1675e6f384de2' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.7.2@sha256:f20d112ff9a97e052a9187063eabbd8d484ce369114d44186e344169a1b3ef6b' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.2.0@sha256:fc9ca69d16e6ebdbd98ed53143d4a0d2212eef60cb638dc71219234e6f427a2c' },
|
||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:1.0.0@sha256:3b70aac36700225945a4a39b5a400c28e010e980879d0dcca76e4a37b04a16ed' }
|
||||
'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.2@sha256:d71d7fa5be9a578aef5f24aceaa0d6fe59ef1e8d2858263669ddf58703d1a963' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.3.0@sha256:b7bc1ca4f4d0603a01369a689129aa273a938ce195fe43d00d42f4f2d5212f50' },
|
||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:1.1.0@sha256:0c1fe4dd6121900624dcb383251ecb0084c3810e095064933de671409d8d6d7b' }
|
||||
}
|
||||
};
|
||||
|
||||
11
src/ldap.js
11
src/ldap.js
@@ -154,7 +154,6 @@ 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
|
||||
}
|
||||
};
|
||||
@@ -392,7 +391,7 @@ function mailAliasSearch(req, res, next) {
|
||||
objectclass: ['nisMailAlias'],
|
||||
objectcategory: 'nisMailAlias',
|
||||
cn: `${alias.name}@${alias.domain}`,
|
||||
rfc822MailMember: `${alias.aliasTarget}@${alias.domain}`
|
||||
rfc822MailMember: `${alias.aliasName}@${alias.aliasDomain}`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -418,7 +417,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) {
|
||||
mail.resolveList(parts[0], parts[1], function (error, resolvedMembers, list) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
@@ -431,6 +430,7 @@ 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
|
||||
}
|
||||
};
|
||||
@@ -618,10 +618,7 @@ 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) { // matched app password
|
||||
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
|
||||
return res.end();
|
||||
}
|
||||
if (appId) return res.end();
|
||||
|
||||
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
maxsize 1M
|
||||
missingok
|
||||
delaycompress
|
||||
# this truncates the original log file and not the rotated one
|
||||
copytruncate
|
||||
}
|
||||
|
||||
@@ -18,6 +19,7 @@
|
||||
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
src/mail.js
59
src/mail.js
@@ -38,7 +38,6 @@ exports = module.exports = {
|
||||
updateMailboxOwner: updateMailboxOwner,
|
||||
removeMailbox: removeMailbox,
|
||||
|
||||
listAliases: listAliases,
|
||||
getAliases: getAliases,
|
||||
setAliases: setAliases,
|
||||
|
||||
@@ -208,7 +207,8 @@ function checkDkim(mailDomain, callback) {
|
||||
|
||||
if (txtRecords.length !== 0) {
|
||||
dkim.value = txtRecords[0].join('');
|
||||
dkim.status = (dkim.value === dkim.expected);
|
||||
const actual = txtToDict(dkim.value);
|
||||
dkim.status = actual.p === dkimKey;
|
||||
}
|
||||
|
||||
callback(null, dkim);
|
||||
@@ -269,7 +269,7 @@ function checkMx(domain, mailFqdn, callback) {
|
||||
if (error) return callback(error, mx);
|
||||
if (mxRecords.length === 0) return callback(null, mx);
|
||||
|
||||
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
|
||||
mx.status = mxRecords.some(mx => mx.exchange === mailFqdn); // this lets use change priority and/or setup backup MX
|
||||
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
|
||||
|
||||
if (mx.status) return callback(null, mx); // MX record is "my."
|
||||
@@ -798,6 +798,7 @@ 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);
|
||||
|
||||
@@ -1125,19 +1126,6 @@ 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');
|
||||
@@ -1161,12 +1149,15 @@ function setAliases(name, domain, aliases, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
for (var i = 0; i < aliases.length; i++) {
|
||||
aliases[i] = aliases[i].toLowerCase();
|
||||
let name = aliases[i].name.toLowerCase();
|
||||
let domain = aliases[i].domain.toLowerCase();
|
||||
|
||||
var error = validateName(aliases[i]);
|
||||
let error = validateName(name);
|
||||
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);
|
||||
|
||||
@@ -1197,10 +1188,11 @@ function getList(name, domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addList(name, domain, members, auditSource, callback) {
|
||||
function addList(name, domain, members, membersOnly, 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');
|
||||
|
||||
@@ -1213,19 +1205,20 @@ function addList(name, domain, members, auditSource, callback) {
|
||||
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
|
||||
}
|
||||
|
||||
mailboxdb.addList(name, domain, members, function (error) {
|
||||
mailboxdb.addList(name, domain, members, membersOnly, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members });
|
||||
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members, membersOnly });
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function updateList(name, domain, members, auditSource, callback) {
|
||||
function updateList(name, domain, members, membersOnly, 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');
|
||||
|
||||
@@ -1241,10 +1234,10 @@ function updateList(name, domain, members, auditSource, callback) {
|
||||
getList(name, domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mailboxdb.updateList(name, domain, members, function (error) {
|
||||
mailboxdb.updateList(name, domain, members, membersOnly, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members });
|
||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members, membersOnly });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -1266,6 +1259,7 @@ 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');
|
||||
@@ -1296,18 +1290,21 @@ 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(); }
|
||||
if (error && error.reason == BoxError.NOT_FOUND) { result.push(member); return iteratorCallback(); } // let it bounce
|
||||
if (error) return iteratorCallback(error);
|
||||
|
||||
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(); }
|
||||
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);
|
||||
}
|
||||
|
||||
toResolve = toResolve.concat(entry.members);
|
||||
iteratorCallback();
|
||||
});
|
||||
}, function (error) {
|
||||
callback(error, result);
|
||||
callback(error, result, list);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ exports = module.exports = {
|
||||
updateList: updateList,
|
||||
del: del,
|
||||
|
||||
listAliases: listAliases,
|
||||
listMailboxes: listMailboxes,
|
||||
getLists: getLists,
|
||||
|
||||
@@ -41,12 +40,14 @@ var assert = require('assert'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
|
||||
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain' ].join(',');
|
||||
|
||||
function postProcess(data) {
|
||||
data.members = safe.JSON.parse(data.membersJson) || [ ];
|
||||
delete data.membersJson;
|
||||
|
||||
data.membersOnly = !!data.membersOnly;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -78,14 +79,15 @@ function updateMailboxOwner(name, domain, ownerId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addList(name, domain, members, callback) {
|
||||
function addList(name, domain, members, membersOnly, 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) VALUES (?, ?, ?, ?, ?)',
|
||||
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members) ], function (error) {
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson, membersOnly) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members), membersOnly ], function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists'));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
@@ -93,14 +95,15 @@ function addList(name, domain, members, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateList(name, domain, members, callback) {
|
||||
function updateList(name, domain, members, membersOnly, 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 = ? WHERE name = ? AND domain = ?',
|
||||
[ JSON.stringify(members), name, domain ], function (error, result) {
|
||||
database.query('UPDATE mailboxes SET membersJson = ?, membersOnly = ? WHERE name = ? AND domain = ?',
|
||||
[ JSON.stringify(members), membersOnly, 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'));
|
||||
|
||||
@@ -123,7 +126,7 @@ function del(name, domain, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// deletes aliases as well
|
||||
database.query('DELETE FROM mailboxes WHERE (name=? OR aliasTarget = ?) AND domain = ?', [ name, name, domain ], function (error, result) {
|
||||
database.query('DELETE FROM mailboxes WHERE ((name=? AND domain=?) OR (aliasName = ? AND aliasDomain=?))', [ name, domain, 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'));
|
||||
|
||||
@@ -285,10 +288,10 @@ function setAliasesForName(name, domain, aliases, callback) {
|
||||
|
||||
var queries = [];
|
||||
// clear existing aliases
|
||||
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
|
||||
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
|
||||
aliases.forEach(function (alias) {
|
||||
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId) VALUES (?, ?, ?, ?, ?)',
|
||||
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId ] });
|
||||
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId ] });
|
||||
});
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
@@ -311,27 +314,10 @@ function getAliasesForName(name, domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT name FROM mailboxes WHERE type = ? AND aliasTarget = ? AND domain = ? ORDER BY name',
|
||||
database.query('SELECT name, domain FROM mailboxes WHERE type = ? AND aliasName = ? AND aliasDomain = ? 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ server {
|
||||
# https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices#25-use-forward-secrecy
|
||||
# ciphers according to https://ssl-config.mozilla.org/#server=nginx&version=1.14.0&config=intermediate&openssl=1.1.1&guideline=5.4
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_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_dhparam /home/yellowtent/boxdata/dhparams.pem;
|
||||
@@ -135,8 +135,21 @@ server {
|
||||
# internal means this is for internal routing and cannot be accessed as URL from browser
|
||||
internal;
|
||||
}
|
||||
location /appstatus.html {
|
||||
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 / {
|
||||
|
||||
@@ -206,9 +206,16 @@ 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.testConfig(backupConfig, function (error) {
|
||||
backups.testProviderConfig(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);
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ function getCertApi(domainObject, callback) {
|
||||
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
users.getOwner(function (error, owner) {
|
||||
options.email = error ? 'support@cloudron.io' : owner.email; // can error if not activated yet
|
||||
options.email = error ? 'webmaster@cloudron.io' : owner.email; // can error if not activated yet
|
||||
|
||||
callback(null, api, options);
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ exports = module.exports = {
|
||||
setMailbox: setMailbox,
|
||||
setLocation: setLocation,
|
||||
setDataDir: setDataDir,
|
||||
setBinds: setBinds,
|
||||
|
||||
stop: stop,
|
||||
start: start,
|
||||
@@ -427,7 +428,7 @@ function importApp(req, res, next) {
|
||||
|
||||
if (req.body.backupConfig) {
|
||||
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if ('password' in backupConfig && typeof backupConfig.password !== 'string') return next(new HttpError(400, 'password 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
|
||||
@@ -645,11 +646,12 @@ function exec(req, res, next) {
|
||||
|
||||
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) {
|
||||
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();
|
||||
|
||||
@@ -683,6 +685,9 @@ 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) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
@@ -760,3 +765,23 @@ 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 }));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -218,8 +218,8 @@ function checkForUpdates(req, res, next) {
|
||||
req.clearTimeout();
|
||||
|
||||
async.series([
|
||||
updateChecker.checkAppUpdates,
|
||||
updateChecker.checkBoxUpdates
|
||||
(done) => updateChecker.checkAppUpdates({ automatic: false }, done),
|
||||
(done) => updateChecker.checkBoxUpdates({ automatic: false }, done),
|
||||
], function () {
|
||||
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
|
||||
});
|
||||
|
||||
@@ -20,7 +20,6 @@ exports = module.exports = {
|
||||
updateMailbox: updateMailbox,
|
||||
removeMailbox: removeMailbox,
|
||||
|
||||
listAliases: listAliases,
|
||||
getAliases: getAliases,
|
||||
setAliases: setAliases,
|
||||
|
||||
@@ -215,22 +214,6 @@ 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');
|
||||
@@ -249,8 +232,10 @@ function setAliases(req, res, next) {
|
||||
|
||||
if (!Array.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array'));
|
||||
|
||||
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'));
|
||||
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'));
|
||||
}
|
||||
|
||||
mail.setAliases(req.params.name, req.params.domain, req.body.aliases, function (error) {
|
||||
@@ -292,8 +277,9 @@ 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, auditSource.fromRequest(req), function (error) {
|
||||
mail.addList(req.body.name, req.params.domain, req.body.members, req.body.membersOnly, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(201, {}));
|
||||
@@ -310,8 +296,9 @@ 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, auditSource.fromRequest(req), function (error) {
|
||||
mail.updateList(req.params.name, req.params.domain, req.body.members, req.body.membersOnly, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
|
||||
@@ -21,7 +21,7 @@ function proxy(req, res, next) {
|
||||
delete req.headers['authorization'];
|
||||
delete req.headers['cookies'];
|
||||
|
||||
addons.getServiceDetails('mail', 'CLOUDRON_MAIL_TOKEN', function (error, addonDetails) {
|
||||
addons.getContainerDetails('mail', 'CLOUDRON_MAIL_TOKEN', function (error, addonDetails) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
parsedUrl.query['access_token'] = addonDetails.token;
|
||||
|
||||
@@ -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 ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if ('password' in backupConfig && typeof backupConfig.password !== 'string') return next(new HttpError(400, 'password 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 or null'));
|
||||
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string'));
|
||||
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'));
|
||||
|
||||
@@ -97,9 +97,8 @@ 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 ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
|
||||
if ('syncConcurrency' in req.body) {
|
||||
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
|
||||
if (req.body.syncConcurrency < 1) return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
|
||||
@@ -107,6 +106,8 @@ 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();
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ function setup(done) {
|
||||
},
|
||||
|
||||
function createSettings(callback) {
|
||||
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' }, callback);
|
||||
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz', retentionPolicy: {} }, callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz', retentionPolicy: {} })
|
||||
], done);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,8 +63,6 @@ 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)));
|
||||
@@ -300,7 +298,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: '30', exchange: settings.mailFqdn() } ];
|
||||
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: settings.mailFqdn() }, { priority: '10', exchange: 'some.other.server' } ];
|
||||
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']];
|
||||
@@ -317,7 +315,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(false);
|
||||
expect(res.body.dns.dkim.status).to.eql(true); // as long as p= matches we are good
|
||||
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');
|
||||
@@ -326,9 +324,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(false);
|
||||
expect(res.body.dns.mx.status).to.eql(true);
|
||||
expect(res.body.dns.mx.expected).to.eql('10 ' + settings.mailFqdn() + '.');
|
||||
expect(res.body.dns.mx.value).to.eql('20 ' + settings.mailFqdn() + '. 30 ' + settings.mailFqdn() + '.');
|
||||
expect(res.body.dns.mx.value).to.eql('20 ' + settings.mailFqdn() + '. 10 some.other.server.');
|
||||
|
||||
expect(res.body.dns.ptr).to.be.an('object');
|
||||
expect(res.body.dns.ptr.expected).to.eql(settings.mailFqdn());
|
||||
@@ -602,7 +600,8 @@ 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.aliasTarget).to.equal(null);
|
||||
expect(res.body.mailbox.aliasName).to.equal(null);
|
||||
expect(res.body.mailbox.aliasDomain).to.equal(null);
|
||||
expect(res.body.mailbox.domain).to.equal(DOMAIN_0.domain);
|
||||
done();
|
||||
});
|
||||
@@ -617,7 +616,8 @@ 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].aliasTarget).to.equal(null);
|
||||
expect(res.body.mailboxes[0].aliasName).to.equal(null);
|
||||
expect(res.body.mailboxes[0].aliasDomain).to.equal(null);
|
||||
expect(res.body.mailboxes[0].domain).to.equal(DOMAIN_0.domain);
|
||||
done();
|
||||
});
|
||||
@@ -655,46 +655,7 @@ describe('Mail API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
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) {
|
||||
it('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 })
|
||||
@@ -704,9 +665,38 @@ 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 + '/aliases/' + MAILBOX_NAME)
|
||||
.send({ aliases: ['hello', 'there'] })
|
||||
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}] })
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
@@ -715,35 +705,17 @@ describe('Mail API', function () {
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
|
||||
superagent.get(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(200);
|
||||
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);
|
||||
expect(res.body.aliases).to.eql([{ name: 'hello', domain: DOMAIN_0.domain}, {name: 'there', domain: DOMAIN_0.domain}]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get fails if mailbox does not exist', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + 'someuserdoesnotexist')
|
||||
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/somerandomuser/aliases')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
@@ -792,7 +764,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}`] })
|
||||
.send({ name: LIST_NAME, members: [ `admin2@${DOMAIN_0.domain}`, `${USERNAME}@${DOMAIN_0.domain}`], membersOnly: false })
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
@@ -802,7 +774,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}`] })
|
||||
.send({ name: LIST_NAME, members: [ `admin2@${DOMAIN_0.domain}`, `${USERNAME}@${DOMAIN_0.domain}`], membersOnly: false })
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(409);
|
||||
@@ -827,9 +799,10 @@ 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.aliasTarget).to.equal(null);
|
||||
expect(res.body.list.aliasName).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();
|
||||
});
|
||||
});
|
||||
@@ -843,9 +816,10 @@ 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].aliasTarget).to.equal(null);
|
||||
expect(res.body.lists[0].aliasName).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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,6 +212,7 @@ function initializeExpressSync() {
|
||||
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/repair', token, authorizeAdmin, routes.apps.load, routes.apps.repair);
|
||||
router.post('/api/v1/apps/:id/update', token, authorizeAdmin, routes.apps.load, routes.apps.update);
|
||||
@@ -265,9 +266,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/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/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/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);
|
||||
|
||||
@@ -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',
|
||||
retentionSecs: 2 * 24 * 60 * 60, // 2 days
|
||||
encryption: null,
|
||||
retentionPolicy: { keepWithinSecs: 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, key, region, prefix, bucket
|
||||
callback(null, JSON.parse(value)); // provider, token, password, region, prefix, bucket
|
||||
});
|
||||
}
|
||||
|
||||
@@ -394,14 +394,19 @@ function setBackupConfig(backupConfig, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getBackupConfig(function (error, curentConfig) {
|
||||
getBackupConfig(function (error, currentConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
backups.injectPrivateFields(backupConfig, curentConfig);
|
||||
backups.injectPrivateFields(backupConfig, currentConfig);
|
||||
|
||||
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) {
|
||||
@@ -423,7 +428,7 @@ function setBackupCredentials(credentials, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// preserve these fields
|
||||
const extra = _.pick(currentConfig, 'retentionSecs', 'intervalSecs', 'copyConcurrency', 'syncConcurrency');
|
||||
const extra = _.pick(currentConfig, 'retentionPolicy', 'intervalSecs', 'copyConcurrency', 'syncConcurrency');
|
||||
|
||||
const backupConfig = _.extend({}, credentials, extra);
|
||||
|
||||
|
||||
@@ -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 = backups.SECRET_PLACEHOLDER;
|
||||
apiConfig.credentials.private_key = constants.SECRET_PLACEHOLDER;
|
||||
return apiConfig;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.credentials.private_key === backups.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
|
||||
if (newConfig.credentials.private_key === constants.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
|
||||
}
|
||||
|
||||
@@ -32,13 +32,13 @@ var assert = require('assert'),
|
||||
EventEmitter = require('events');
|
||||
|
||||
function removePrivateFields(apiConfig) {
|
||||
// in-place removal of tokens and api keys with domains.SECRET_PLACEHOLDER
|
||||
// in-place removal of tokens and api keys with constants.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 domains.SECRET_PLACEHOLDER
|
||||
// in-place injection of tokens and api keys which came in with constants.SECRET_PLACEHOLDER
|
||||
}
|
||||
|
||||
function upload(apiConfig, backupFilePath, sourceStream, callback) {
|
||||
|
||||
@@ -22,15 +22,16 @@ 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');
|
||||
S3BlockReadStream = require('s3-block-read-stream'),
|
||||
_ = require('underscore');
|
||||
|
||||
// test only
|
||||
var originalAWS;
|
||||
@@ -429,7 +430,7 @@ function testConfig(apiConfig, callback) {
|
||||
Body: 'testcontent'
|
||||
};
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
var s3 = new AWS.S3(_.omit(credentials, 'retryDelayOptions', 'maxRetries'));
|
||||
s3.putObject(params, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message || error.code)); // DO sets 'code'
|
||||
|
||||
@@ -448,10 +449,10 @@ function testConfig(apiConfig, callback) {
|
||||
}
|
||||
|
||||
function removePrivateFields(apiConfig) {
|
||||
apiConfig.secretAccessKey = backups.SECRET_PLACEHOLDER;
|
||||
apiConfig.secretAccessKey = constants.SECRET_PLACEHOLDER;
|
||||
return apiConfig;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.secretAccessKey === backups.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
|
||||
if (newConfig.secretAccessKey === constants.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ 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'),
|
||||
@@ -68,6 +69,69 @@ 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');
|
||||
@@ -78,9 +142,9 @@ describe('backups', function () {
|
||||
database._clear,
|
||||
settings.setBackupConfig.bind(null, {
|
||||
provider: 'filesystem',
|
||||
key: 'enckey',
|
||||
password: 'supersecret',
|
||||
backupFolder: BACKUP_DIR,
|
||||
retentionSecs: 1,
|
||||
retentionPolicy: { keepWithinSecs: 1 },
|
||||
format: 'tgz'
|
||||
})
|
||||
], done);
|
||||
@@ -95,7 +159,8 @@ describe('backups', function () {
|
||||
describe('cleanup', function () {
|
||||
var BACKUP_0 = {
|
||||
id: 'backup-box-0',
|
||||
version: '1.0.0',
|
||||
encryptionVersion: null,
|
||||
packageVersion: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_BOX,
|
||||
dependsOn: [ 'backup-app-00', 'backup-app-01' ],
|
||||
manifest: null,
|
||||
@@ -104,7 +169,8 @@ describe('backups', function () {
|
||||
|
||||
var BACKUP_0_APP_0 = {
|
||||
id: 'backup-app-00',
|
||||
version: '1.0.0',
|
||||
encryptionVersion: null,
|
||||
packageVersion: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_APP,
|
||||
dependsOn: [],
|
||||
manifest: null,
|
||||
@@ -113,7 +179,8 @@ describe('backups', function () {
|
||||
|
||||
var BACKUP_0_APP_1 = {
|
||||
id: 'backup-app-01',
|
||||
version: '1.0.0',
|
||||
encryptionVersion: null,
|
||||
packageVersion: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_APP,
|
||||
dependsOn: [],
|
||||
manifest: null,
|
||||
@@ -122,7 +189,8 @@ describe('backups', function () {
|
||||
|
||||
var BACKUP_1 = {
|
||||
id: 'backup-box-1',
|
||||
version: '1.0.0',
|
||||
encryptionVersion: null,
|
||||
packageVersion: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_BOX,
|
||||
dependsOn: [ 'backup-app-10', 'backup-app-11' ],
|
||||
manifest: null,
|
||||
@@ -131,7 +199,8 @@ describe('backups', function () {
|
||||
|
||||
var BACKUP_1_APP_0 = {
|
||||
id: 'backup-app-10',
|
||||
version: '1.0.0',
|
||||
encryptionVersion: null,
|
||||
packageVersion: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_APP,
|
||||
dependsOn: [],
|
||||
manifest: null,
|
||||
@@ -140,7 +209,8 @@ describe('backups', function () {
|
||||
|
||||
var BACKUP_1_APP_1 = {
|
||||
id: 'backup-app-11',
|
||||
version: '1.0.0',
|
||||
encryptionVersion: null,
|
||||
packageVersion: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_APP,
|
||||
dependsOn: [],
|
||||
manifest: null,
|
||||
@@ -271,25 +341,26 @@ describe('backups', function () {
|
||||
describe('filesystem', function () {
|
||||
var backupInfo1;
|
||||
|
||||
var gBackupConfig = {
|
||||
var backupConfig = {
|
||||
provider: 'filesystem',
|
||||
backupFolder: path.join(os.tmpdir(), 'backups-test-filesystem'),
|
||||
format: 'tgz'
|
||||
format: 'tgz',
|
||||
retentionPolicy: { keepWithinSecs: 10000 }
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
rimraf.sync(gBackupConfig.backupFolder);
|
||||
rimraf.sync(backupConfig.backupFolder);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
rimraf.sync(gBackupConfig.backupFolder);
|
||||
rimraf.sync(backupConfig.backupFolder);
|
||||
done();
|
||||
});
|
||||
|
||||
it('fails to set backup config for non-existing folder', function (done) {
|
||||
settings.setBackupConfig(gBackupConfig, function (error) {
|
||||
settings.setBackupConfig(backupConfig, function (error) {
|
||||
expect(error).to.be.a(BoxError);
|
||||
expect(error.reason).to.equal(BoxError.BAD_FIELD);
|
||||
|
||||
@@ -298,9 +369,9 @@ describe('backups', function () {
|
||||
});
|
||||
|
||||
it('succeeds to set backup config', function (done) {
|
||||
mkdirp.sync(gBackupConfig.backupFolder);
|
||||
mkdirp.sync(backupConfig.backupFolder);
|
||||
|
||||
settings.setBackupConfig(gBackupConfig, function (error) {
|
||||
settings.setBackupConfig(backupConfig, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
done();
|
||||
@@ -313,8 +384,8 @@ describe('backups', function () {
|
||||
|
||||
createBackup(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
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);
|
||||
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);
|
||||
|
||||
backupInfo1 = result;
|
||||
|
||||
@@ -328,9 +399,9 @@ describe('backups', function () {
|
||||
|
||||
createBackup(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
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
|
||||
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
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -418,7 +418,9 @@ describe('database', function () {
|
||||
dataDir: null,
|
||||
tags: [],
|
||||
label: null,
|
||||
taskId: null
|
||||
taskId: null,
|
||||
binds: {},
|
||||
servicesConfig: {}
|
||||
};
|
||||
|
||||
it('cannot delete referenced domain', function (done) {
|
||||
@@ -889,7 +891,9 @@ describe('database', function () {
|
||||
dataDir: null,
|
||||
tags: [],
|
||||
label: null,
|
||||
taskId: null
|
||||
taskId: null,
|
||||
binds: {},
|
||||
servicesConfig: {}
|
||||
};
|
||||
|
||||
var APP_1 = {
|
||||
@@ -920,7 +924,9 @@ describe('database', function () {
|
||||
dataDir: null,
|
||||
tags: [],
|
||||
label: null,
|
||||
taskId: null
|
||||
taskId: null,
|
||||
binds: {},
|
||||
servicesConfig: {}
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
@@ -1318,7 +1324,8 @@ describe('database', function () {
|
||||
it('add succeeds', function (done) {
|
||||
var backup = {
|
||||
id: 'backup-box',
|
||||
version: '1.0.0',
|
||||
encryptionVersion: 2,
|
||||
packageVersion: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_BOX,
|
||||
dependsOn: [ 'dep1' ],
|
||||
manifest: null,
|
||||
@@ -1334,7 +1341,8 @@ describe('database', function () {
|
||||
it('get succeeds', function (done) {
|
||||
backupdb.get('backup-box', function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.version).to.be('1.0.0');
|
||||
expect(result.encryptionVersion).to.be(2);
|
||||
expect(result.packageVersion).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']);
|
||||
@@ -1359,7 +1367,8 @@ describe('database', function () {
|
||||
expect(results.length).to.be(1);
|
||||
|
||||
expect(results[0].id).to.be('backup-box');
|
||||
expect(results[0].version).to.be('1.0.0');
|
||||
expect(results[0].encryptionVersion).to.be(2);
|
||||
expect(results[0].packageVersion).to.be('1.0.0');
|
||||
expect(results[0].dependsOn).to.eql(['dep1']);
|
||||
expect(results[0].manifest).to.eql(null);
|
||||
|
||||
@@ -1384,7 +1393,8 @@ describe('database', function () {
|
||||
it('add app succeeds', function (done) {
|
||||
var backup = {
|
||||
id: 'app_appid_123',
|
||||
version: '1.0.0',
|
||||
encryptionVersion: null,
|
||||
packageVersion: '1.0.0',
|
||||
type: backupdb.BACKUP_TYPE_APP,
|
||||
dependsOn: [ ],
|
||||
manifest: { foo: 'bar' },
|
||||
@@ -1400,7 +1410,8 @@ describe('database', function () {
|
||||
it('get succeeds', function (done) {
|
||||
backupdb.get('app_appid_123', function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.version).to.be('1.0.0');
|
||||
expect(result.encryptionVersion).to.be(null);
|
||||
expect(result.packageVersion).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([]);
|
||||
@@ -1416,7 +1427,8 @@ describe('database', function () {
|
||||
expect(results.length).to.be(1);
|
||||
|
||||
expect(results[0].id).to.be('app_appid_123');
|
||||
expect(results[0].version).to.be('1.0.0');
|
||||
expect(results[0].encryptionVersion).to.be(null);
|
||||
expect(results[0].packageVersion).to.be('1.0.0');
|
||||
expect(results[0].dependsOn).to.eql([]);
|
||||
expect(results[0].manifest).to.eql({ foo: 'bar' });
|
||||
|
||||
@@ -1829,7 +1841,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('can set alias', function (done) {
|
||||
mailboxdb.setAliasesForName('support', DOMAIN_0.domain, [ 'support2', 'help' ], function (error) {
|
||||
mailboxdb.setAliasesForName('support', DOMAIN_0.domain, [ { name: 'support2', domain: DOMAIN_0.domain }, { name: 'help', domain: DOMAIN_0.domain } ], function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
@@ -1839,8 +1851,10 @@ 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]).to.be('help');
|
||||
expect(results[1]).to.be('support2');
|
||||
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);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -1849,18 +1863,8 @@ 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.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');
|
||||
expect(result.aliasName).to.be('support');
|
||||
expect(result.aliasDomain).to.be(DOMAIN_0.domain);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ 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 = {
|
||||
@@ -87,6 +88,7 @@ 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),
|
||||
function (callback) {
|
||||
users.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE, function (error, result) {
|
||||
@@ -106,7 +108,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, [ USER_0_ALIAS.toLocaleLowerCase() ], done),
|
||||
(done) => mailboxdb.setAliasesForName(USER_0.username.toLowerCase(), DOMAIN_0.domain, [ { name: USER_0_ALIAS.toLocaleLowerCase(), domain: DOMAIN_0.domain} ], 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' }]),
|
||||
@@ -788,7 +790,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' ], done);
|
||||
mailboxdb.addList('devs', DOMAIN_0.domain, [ USER_0.username.toLowerCase() + '@' + DOMAIN_0.domain , USER_1.username.toLowerCase() + '@external.com' ], false /* membersOnly */, done);
|
||||
});
|
||||
|
||||
it('get specific list', function (done) {
|
||||
|
||||
@@ -196,7 +196,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: 'support@cloudron.io', 'performHttpAuthorization': false, 'prod': false, 'wildcard': false });
|
||||
expect(options).to.eql({ email: 'webmaster@cloudron.io', 'performHttpAuthorization': false, 'prod': false, 'wildcard': false });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ 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'),
|
||||
@@ -95,10 +94,10 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/boxupdate')
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken' })
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', automatic: false })
|
||||
.reply(204, { } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
updatechecker.checkBoxUpdates({ automatic: false }, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box).to.be(null);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -112,10 +111,10 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/boxupdate')
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken' })
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', automatic: false })
|
||||
.reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz', sourceTarballSigUrl: 'box.tar.gz.sig', boxVersionsUrl: 'box.versions', boxVersionsSigUrl: 'box.versions.sig' } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
updatechecker.checkBoxUpdates({ automatic: false }, 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');
|
||||
@@ -130,10 +129,10 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/boxupdate')
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken' })
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', automatic: false })
|
||||
.reply(404, { version: '2.0.0-pre.0', changelog: [''], sourceTarballUrl: 'box-pre.tar.gz' } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
updatechecker.checkBoxUpdates({ automatic: false }, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box).to.be(null);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -165,10 +164,10 @@ describe('updatechecker - box - automatic (no email)', function () {
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/boxupdate')
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken' })
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', automatic: false })
|
||||
.reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz', sourceTarballSigUrl: 'box.tar.gz.sig', boxVersionsUrl: 'box.versions', boxVersionsSigUrl: 'box.versions.sig' } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
updatechecker.checkBoxUpdates({ automatic: false }, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -200,10 +199,10 @@ describe('updatechecker - box - automatic free (email)', function () {
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/boxupdate')
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken' })
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', automatic: false })
|
||||
.reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz', sourceTarballSigUrl: 'box.tar.gz.sig', boxVersionsUrl: 'box.versions', boxVersionsSigUrl: 'box.versions.sig' } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
updatechecker.checkBoxUpdates({ automatic: false }, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -265,10 +264,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 })
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version, automatic: false })
|
||||
.reply(204, { } );
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
updatechecker.checkAppUpdates({ automatic: false }, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().apps).to.eql({});
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -282,10 +281,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 })
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version, automatic: false })
|
||||
.reply(500, { update: { manifest: { version: '1.0.0', changelog: '* some changes' } } } );
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
updatechecker.checkAppUpdates({ automatic: false }, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().apps).to.eql({});
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -299,10 +298,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 })
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version, automatic: false })
|
||||
.reply(200, { manifest: { version: '2.0.0', changelog: '* some changes' } } );
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
updatechecker.checkAppUpdates({ automatic: false }, 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();
|
||||
@@ -314,7 +313,7 @@ describe('updatechecker - app - manual (email)', function () {
|
||||
it('does not offer old version', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
updatechecker.checkAppUpdates({ automatic: false }, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().apps).to.eql({ });
|
||||
checkMails(0, done);
|
||||
@@ -374,10 +373,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 })
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version, automatic: false })
|
||||
.reply(200, { manifest: { version: '2.0.0', changelog: 'c' } } );
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
updatechecker.checkAppUpdates({ automatic: false }, 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();
|
||||
@@ -439,10 +438,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 })
|
||||
.query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version, automatic: false })
|
||||
.reply(200, { manifest: { version: '2.0.0', changelog: 'c' } } );
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
updatechecker.checkAppUpdates({ automatic: false }, 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();
|
||||
|
||||
@@ -62,7 +62,8 @@ function resetAppUpdateInfo(appId) {
|
||||
}
|
||||
}
|
||||
|
||||
function checkAppUpdates(callback) {
|
||||
function checkAppUpdates(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Checking App Updates');
|
||||
@@ -84,7 +85,7 @@ function checkAppUpdates(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, function (error, updateInfo) {
|
||||
appstore.getAppUpdate(app, options, function (error, updateInfo) {
|
||||
if (error) {
|
||||
debug('Error getting app update info for %s', app.id, error);
|
||||
return iteratorDone(); // continue to next
|
||||
@@ -136,14 +137,15 @@ function checkAppUpdates(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkBoxUpdates(callback) {
|
||||
function checkBoxUpdates(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Checking Box Updates');
|
||||
|
||||
gBoxUpdateInfo = null;
|
||||
|
||||
appstore.getBoxUpdate(function (error, updateInfo) {
|
||||
appstore.getBoxUpdate(options, function (error, updateInfo) {
|
||||
if (error || !updateInfo) return callback(error);
|
||||
|
||||
gBoxUpdateInfo = updateInfo;
|
||||
|
||||
@@ -64,13 +64,16 @@ function gpgVerify(file, sig, callback) {
|
||||
debug(`gpgVerify: ${cmd}`);
|
||||
|
||||
child_process.exec(cmd, { encoding: 'utf8' }, function (error, stdout, stderr) {
|
||||
if (error) return callback(new BoxError(BoxError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not verified`));
|
||||
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 (stdout.indexOf('[GNUPG:] VALIDSIG 0EADB19CDDA23CD0FE71E3470A372F8703C493CC')) return callback();
|
||||
if (stdout.indexOf('[GNUPG:] VALIDSIG 0EADB19CDDA23CD0FE71E3470A372F8703C493CC') !== -1) 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`));
|
||||
return callback(new BoxError(BoxError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not verified (bad sig)`));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -584,13 +584,13 @@ function createInvite(user, callback) {
|
||||
|
||||
if (user.source) return callback(new BoxError(BoxError.CONFLICT, 'User is from an external directory'));
|
||||
|
||||
let resetToken = hat(256);
|
||||
user.resetToken = resetToken;
|
||||
const resetToken = hat(256), resetTokenCreationTime = new Date();
|
||||
|
||||
userdb.update(user.id, { resetToken }, function (error) {
|
||||
userdb.update(user.id, { resetToken, resetTokenCreationTime }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, { resetToken: user.resetToken, inviteLink: inviteLink(user) });
|
||||
user.resetToken = resetToken;
|
||||
callback(null, { resetToken, inviteLink: inviteLink(user) });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user