Compare commits

..

12 Commits

Author SHA1 Message Date
Girish Ramakrishnan bb10d6ed71 caas can be a provider 2018-02-17 22:26:09 -08:00
Girish Ramakrishnan 304d9dafb8 restart mail container when mail.ini changes 2018-02-17 18:17:28 -08:00
Girish Ramakrishnan 9771de5d97 1.11.0 changes 2018-02-16 16:36:25 -08:00
Girish Ramakrishnan b317ac8258 Bump mail container for Haraka 2.8.17 2018-02-16 16:04:16 -08:00
Johannes Zellner 7f8060dd14 Report dependency error for clone if backup or domain was not found 2018-02-16 16:03:21 -08:00
Girish Ramakrishnan 7d48887428 Enable auto-updates for major versions
Cloudron is always rolling releases and we never break compat
2018-02-16 16:03:06 -08:00
Johannes Zellner 6f6afa1b6a Add 1.10.2 changes 2018-02-07 15:34:24 +01:00
Johannes Zellner 3634e47794 Keep the invite email for users, which have not yet setup a username 2018-02-07 15:33:35 +01:00
Girish Ramakrishnan 5be39bc271 createReleaseTarball: Make sure we pick the current branch on webadmin 2018-02-06 16:18:42 -08:00
Girish Ramakrishnan 6b55d7585c Add 1.10.1 changes 2018-02-06 16:11:21 -08:00
Johannes Zellner 8e4b3f9a4a Drop users email unique constraint for the migration timeframe 2018-02-06 12:19:13 +01:00
Girish Ramakrishnan 15cf0c3c19 Do not allow dns setup and restore to run in parallel
In the e2e, we did not check the webadminStatus after a dnsSetup
and immediately rushed into restore. This ended up mangling the
cert/key files of the admin domain.
2018-02-04 15:08:48 -08:00
81 changed files with 1369 additions and 2260 deletions
-1
View File
@@ -2,7 +2,6 @@
"node": true,
"browser": true,
"unused": true,
"multistr": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
-13
View File
@@ -1210,16 +1210,3 @@
* Report dependency error for clone if backup or domain was not found
* Enable auto-updates for major versions
[2.0.0]
* Multi-domain support
* Update Haraka to 2.8.18
* Split box and app autoupdate pattern settings
* Stop and disable any pre-installed postfix server
* Migrate altDomain as a manual DNS provider
* Use node's native dns resolve instead of dig
* DNS records can now be a A record or a CNAME record
* Fix generation of fallback certificates to include naked domain
* Merge multi-string DKIM records
* scheduler: do not start cron jobs all at once
* scheduler: give cron jobs a grace period of 30 minutes to complete
+1 -1
View File
@@ -59,6 +59,6 @@ the containers in the Cloudron.
## Community
* [Forum](https://forum.cloudron.io/)
* [Chat](https://chat.cloudron.io/)
* [Support](mailto:support@cloudron.io)
-4
View File
@@ -105,7 +105,3 @@ systemctl disable bind9 || true
systemctl stop dnsmasq || true
systemctl disable dnsmasq || true
# on ssdnodes postfix seems to run by default
systemctl stop postfix || true
systemctl disable postfix || true
@@ -1,24 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM settings WHERE name=?', ['autoupdate_pattern'], function (error, results) {
if (error || results.length === 0) return callback(error); // will use defaults from box code
// migrate the 'daily' update pattern
var appUpdatePattern = results[0].value;
if (appUpdatePattern === '00 00 1,3,5,23 * * *') appUpdatePattern = '00 30 1,3,5,23 * * *';
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'DELETE FROM settings WHERE name=?', ['autoupdate_pattern']),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['app_autoupdate_pattern', appUpdatePattern]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,121 +0,0 @@
'use strict';
var async = require('async'),
crypto = require('crypto'),
fs = require('fs'),
os = require('os'),
path = require('path'),
safe = require('safetydance'),
tldjs = require('tldjs');
exports.up = function(db, callback) {
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, callback) {
if (!app.altDomain) {
console.log('App %s does not use altDomain, skip', app.id);
return callback();
}
const domain = tldjs.getDomain(app.altDomain);
const subdomain = tldjs.getSubdomain(app.altDomain);
const mailboxName = (subdomain ? subdomain : JSON.parse(app.manifestJson).title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
console.log('App %s is on domain %s and subdomain %s with mailbox', app.id, domain, subdomain, mailboxName);
async.series([
// Add domain if not exists
function (callback) {
const query = 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, domain, 'manual', JSON.stringify({}), JSON.stringify({ provider: 'letsencrypt-prod' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s', domain);
// ensure we have a fallback cert for the newly added domain. This is the same as in reverseproxy.js
// WARNING this will only work on the cloudron itself not during local testing!
const certFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.cert`;
const keyFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.key`;
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
let certCommand = `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`;
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
if (!safe.child_process.execSync(certCommand)) return callback(safe.error.message);
safe.fs.unlinkSync(configFile);
}
callback();
});
},
// Add domain to mail table if not exists
function (callback) {
const query = 'INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, 0, 1, '[]', JSON.stringify({ provider: 'cloudron-smtp' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s to mail table', domain);
callback();
});
},
// Remove old mailbox record if any
function (callback) {
const query = 'DELETE FROM mailboxes WHERE ownerId=?';
const args = [ app.id ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Cleaned up mailbox record for app %s', app.id);
callback();
});
},
// Add new mailbox record
function (callback) {
const query = 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)';
const args = [ mailboxName, domain, app.id, 'app' /* mailboxdb.TYPE_APP */ ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Added mailbox record for app %s', app.id);
callback();
});
},
// Update app record
function (callback) {
const query = 'UPDATE apps SET location=?, domain=?, altDomain=? WHERE id=?';
const args = [ subdomain, domain, '', app.id ];
db.runSql(query, args, function (error) {
if (error) return error;
console.log('Updated app %s with new domain', app.id);
callback();
});
}
], callback);
}, function (error) {
if (error) return callback(error);
// finally drop the altDomain db field
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', [], callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', [], callback);
};
@@ -1,19 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_domain_constraint FOREIGN KEY(domain) REFERENCES mail(domain)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint', function (error) {
if (error) console.error(error);
callback(error);
});
};
+18 -16
View File
@@ -72,6 +72,7 @@ CREATE TABLE IF NOT EXISTS apps(
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
altDomain VARCHAR(256),
xFrameOptions VARCHAR(512),
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
debugModeJson TEXT, // options for development mode
@@ -80,8 +81,8 @@ CREATE TABLE IF NOT EXISTS apps(
// the following fields do not belong here, they can be removed when we use a queue for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config to apptask (update)
oldConfigJson TEXT, // used to pass old config for apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config for apptask (update)
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY(id));
@@ -133,6 +134,21 @@ CREATE TABLE IF NOT EXISTS eventlog(
PRIMARY KEY (id));
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
aliasTarget VARCHAR(128), /* the target name type is an alias */
creationTime TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES domains(domain),
UNIQUE (name, domain));
CREATE TABLE IF NOT EXISTS domains(
domain VARCHAR(128) NOT NULL UNIQUE, /* if this needs to be larger, InnoDB has a limit of 767 bytes for PRIMARY KEY values! */
zoneName VARCHAR(128) NOT NULL, /* this mostly contains the domain itself again */
@@ -158,18 +174,4 @@ CREATE TABLE IF NOT EXISTS mail(
CHARACTER SET utf8 COLLATE utf8_bin;
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
aliasTarget VARCHAR(128), /* the target name type is an alias */
creationTime TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain),
UNIQUE (name, domain));
+225 -252
View File
@@ -5,76 +5,117 @@
"requires": true,
"dependencies": {
"@google-cloud/common": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.16.1.tgz",
"integrity": "sha512-1sufDsSfgJ7fuBLq+ux8t3TlydMlyWl9kPZx2WdLINkGtf5RjvXX6EWYZiCMKe8flJ3oC0l95j5atN2uX5n3rg==",
"version": "0.13.6",
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.13.6.tgz",
"integrity": "sha1-qdjhN7xCmkSrqWif5qDkMxeE+FM=",
"requires": {
"array-uniq": "1.0.3",
"arrify": "1.0.1",
"concat-stream": "1.6.0",
"create-error-class": "3.0.2",
"duplexify": "3.5.3",
"duplexify": "3.5.1",
"ent": "2.2.0",
"extend": "3.0.1",
"google-auto-auth": "0.9.4",
"google-auto-auth": "0.7.2",
"is": "3.2.1",
"log-driver": "1.2.5",
"methmeth": "1.1.0",
"modelo": "4.2.3",
"modelo": "4.2.0",
"request": "2.83.0",
"retry-request": "3.3.1",
"split-array-stream": "1.0.3",
"stream-events": "1.0.2",
"string-format-obj": "1.1.1",
"string-format-obj": "1.1.0",
"through2": "2.0.3"
}
},
"@google-cloud/dns": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@google-cloud/dns/-/dns-0.7.1.tgz",
"integrity": "sha512-rOq14I6vUc/Fd+8/cY4wCd6af8/+eN2wTIUSAQQM3zccn3HxgmG9mW54Wuq2iI/LBUYP5Z/+H4MNj3ZrkRIxIA==",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@google-cloud/dns/-/dns-0.7.0.tgz",
"integrity": "sha512-WL9PXmWiXOAKWq06nLzrMvJdumqFGOsB9q1f/DeCK7a7b/iihkyuQGNsSvpy0pf2FPYmDc8qEFtWjRw9xIApuw==",
"requires": {
"@google-cloud/common": "0.16.1",
"@google-cloud/common": "0.13.6",
"arrify": "1.0.1",
"dns-zonefile": "0.2.2",
"extend": "3.0.1",
"is": "3.2.1",
"methmeth": "1.1.0",
"string-format-obj": "1.1.1"
"string-format-obj": "1.1.0"
}
},
"@google-cloud/storage": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-1.6.0.tgz",
"integrity": "sha512-yQ63bJYoiwY220gn/KdTLPoHppAPwFHfG7VFLPwJ+1R5U1eqUN5XV2a7uPj1szGF8/gxlKm2UbE8DgoJJ76DFw==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-1.5.1.tgz",
"integrity": "sha512-7yaYzFWQYgi+n1dw/i5ysgzHD3ACJndxR8CbFnunQNT3NduolYSTr5WbYwWdhN5/7QiKx9g+obgP4tVz904bXw==",
"requires": {
"@google-cloud/common": "0.16.1",
"@google-cloud/common": "0.15.1",
"arrify": "1.0.1",
"async": "2.6.0",
"compressible": "2.0.13",
"concat-stream": "1.6.0",
"create-error-class": "3.0.2",
"duplexify": "3.5.3",
"duplexify": "3.5.1",
"extend": "3.0.1",
"gcs-resumable-upload": "0.9.0",
"gcs-resumable-upload": "0.8.2",
"hash-stream-validation": "0.2.1",
"is": "3.2.1",
"mime": "2.2.0",
"mime-types": "2.1.17",
"once": "1.4.0",
"pumpify": "1.4.0",
"pumpify": "1.3.5",
"request": "2.83.0",
"safe-buffer": "5.1.1",
"snakeize": "0.1.0",
"stream-events": "1.0.2",
"string-format-obj": "1.1.1",
"string-format-obj": "1.1.0",
"through2": "2.0.3"
},
"dependencies": {
"mime": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.2.0.tgz",
"integrity": "sha512-0Qz9uF1ATtl8RKJG4VRfOymh7PyEor6NbrI/61lRfuRe4vx9SNATrvAeTj2EWVRKjEQGskrzWkJBBY5NbaVHIA=="
"@google-cloud/common": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.15.1.tgz",
"integrity": "sha512-cnVtHLvyiSQvb1RzXWDp7PA1sA8Jmc47+wp/xwHwdGOlQZfKog5iluZ0C/LB8iklFXpcTwlNMorqLuZ/qH0DDA==",
"requires": {
"array-uniq": "1.0.3",
"arrify": "1.0.1",
"concat-stream": "1.6.0",
"create-error-class": "3.0.2",
"duplexify": "3.5.1",
"ent": "2.2.0",
"extend": "3.0.1",
"google-auto-auth": "0.8.1",
"is": "3.2.1",
"log-driver": "1.2.5",
"methmeth": "1.1.0",
"modelo": "4.2.0",
"request": "2.83.0",
"retry-request": "3.3.1",
"split-array-stream": "1.0.3",
"stream-events": "1.0.2",
"string-format-obj": "1.1.0",
"through2": "2.0.3"
}
},
"google-auth-library": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.12.0.tgz",
"integrity": "sha512-79qCXtJ1VweBmmLr4yLq9S4clZB2p5Y+iACvuKk9gu4JitEnPc+bQFmYvtCYehVR44MQzD1J8DVmYW2w677IEw==",
"requires": {
"gtoken": "1.2.3",
"jws": "3.1.4",
"lodash.isstring": "4.0.1",
"lodash.merge": "4.6.0",
"request": "2.83.0"
}
},
"google-auto-auth": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.8.1.tgz",
"integrity": "sha512-v5a4mHIkhvbtKNILxnOYgOw+cin/jfLR0pEj1ids2jn9p0OyxYUXjSJbCEciuAorQao9Y55w0zJIc8yW1rIPaA==",
"requires": {
"async": "2.6.0",
"gcp-metadata": "0.3.1",
"google-auth-library": "0.12.0",
"request": "2.83.0"
}
}
}
},
@@ -118,11 +159,11 @@
}
},
"JSONStream": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz",
"integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz",
"integrity": "sha1-dDSdDYlSK3HzDwoD/5vSDKbxKsA=",
"requires": {
"jsonparse": "1.3.1",
"jsonparse": "0.0.5",
"through": "2.3.8"
}
},
@@ -373,11 +414,12 @@
}
},
"aws-sdk": {
"version": "2.201.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.201.0.tgz",
"integrity": "sha1-ekUoJi2Ql6vjfr9Oa/nRhR20cAM=",
"version": "2.170.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.170.0.tgz",
"integrity": "sha1-T5z9JqJo72opadNHXtgTS7OJlSU=",
"requires": {
"buffer": "4.9.1",
"crypto-browserify": "1.0.9",
"events": "1.1.1",
"jmespath": "0.15.0",
"querystring": "0.2.0",
@@ -386,13 +428,6 @@
"uuid": "3.1.0",
"xml2js": "0.4.17",
"xmlbuilder": "4.2.1"
},
"dependencies": {
"uuid": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
}
}
},
"aws-sign2": {
@@ -405,15 +440,6 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
},
"axios": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.17.1.tgz",
"integrity": "sha1-LY4+XQvb1zJ/kbyBT1xXZg+Bgk0=",
"requires": {
"follow-redirects": "1.4.1",
"is-buffer": "1.1.6"
}
},
"backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
@@ -443,9 +469,9 @@
}
},
"base64-js": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.3.tgz",
"integrity": "sha512-MsAhsUW1GxCdgYSO6tAfZrNapmUKk7mWx/k5mFY/A1gBtkaCaNapTg+FExCw1r9yeaZhqx/xPg43xgTFH6KL5w=="
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz",
"integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw=="
},
"base64url": {
"version": "2.0.0",
@@ -599,7 +625,7 @@
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"requires": {
"base64-js": "1.2.3",
"base64-js": "1.2.1",
"ieee754": "1.1.8",
"isarray": "1.0.0"
}
@@ -625,10 +651,21 @@
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
"requires": {
"dtrace-provider": "0.8.6",
"dtrace-provider": "0.8.5",
"moment": "2.19.4",
"mv": "2.1.1",
"safe-json-stringify": "1.1.0"
"safe-json-stringify": "1.0.4"
},
"dependencies": {
"dtrace-provider": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.5.tgz",
"integrity": "sha1-mOu6Ihr6xG4cOf02hY2Pk2dSS5I=",
"optional": true,
"requires": {
"nan": "2.8.0"
}
}
}
},
"bytes": {
@@ -825,7 +862,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
"integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
"dev": true,
"requires": {
"string-width": "1.0.2",
"strip-ansi": "3.0.1",
@@ -836,7 +872,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
"code-point-at": "1.1.0",
"is-fullwidth-code-point": "1.0.0",
@@ -1009,21 +1044,6 @@
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"compressible": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.13.tgz",
"integrity": "sha1-DRAgq5JLL9tNYnmHXH1tq6a6p6k=",
"requires": {
"mime-db": "1.33.0"
},
"dependencies": {
"mime-db": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
}
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1055,7 +1075,7 @@
"requires": {
"dot-prop": "4.2.0",
"graceful-fs": "4.1.11",
"make-dir": "1.2.0",
"make-dir": "1.1.0",
"unique-string": "1.0.0",
"write-file-atomic": "2.3.0",
"xdg-basedir": "3.0.0"
@@ -1288,6 +1308,11 @@
}
}
},
"crypto-browserify": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz",
"integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA="
},
"crypto-random-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
@@ -1525,9 +1550,9 @@
"dev": true
},
"db-migrate": {
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/db-migrate/-/db-migrate-0.10.5.tgz",
"integrity": "sha512-jddRpFIxe+24U9frQBz51wtjFyVu2LnJ4QPcNRiiR4Hvuu8BKwdN2Ez42M6hLGbl7v1rXL2u+bUcNTvPyL/OwQ==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/db-migrate/-/db-migrate-0.10.2.tgz",
"integrity": "sha512-uU3AXQ89DPKAePCHHoZSJRnLam6/zgOBhgSXVXB9Z3JeHms5OMTQuCdj1leUwYvI6ygHdQ3Z9CS6S9yOuELdcw==",
"requires": {
"balanced-match": "0.4.2",
"bluebird": "3.5.1",
@@ -1537,13 +1562,14 @@
"final-fs": "1.6.1",
"inflection": "1.12.0",
"mkdirp": "0.5.1",
"moment": "2.19.4",
"optimist": "0.6.1",
"parse-database-url": "0.3.0",
"pkginfo": "0.4.1",
"prompt": "1.0.0",
"rc": "1.2.5",
"rc": "1.2.2",
"resolve": "1.5.0",
"semver": "5.5.0",
"semver": "5.4.1",
"tunnel-ssh": "4.1.3"
}
},
@@ -1709,16 +1735,24 @@
"integrity": "sha1-PqIQoYmgD3Zn93Cnl0KNjmYnJn8="
},
"docker-modem": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.5.tgz",
"integrity": "sha512-i3J4TYW9iNp+nWzkgGvj9UwSmT6ZUFg2OsjRlUraHCaDCv8z6f0fN3q4ur0Qq27/1GPYXSjShGaE7fDznIJKUg==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.4.tgz",
"integrity": "sha512-pkXB9p7KWagegOXm2NsbVDBluQQLCBJzX9uYJzVbL6CHwe4d2sSbcACJ4K8ISX1l1JUUmFSiwNkBKc1uTiU4MA==",
"requires": {
"JSONStream": "1.3.2",
"debug": "3.1.0",
"JSONStream": "0.10.0",
"debug": "2.6.9",
"readable-stream": "1.0.34",
"split-ca": "1.0.1"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
@@ -1743,12 +1777,12 @@
}
},
"dockerode": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.4.tgz",
"integrity": "sha512-esqrDATdckYhkOFn4BSOrqnkj3jgBkHT07uEqTRwK6na4/Rg60vjXWRopv2BbRpvFruMmKvOSNVY4MbmVBUnWw==",
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.3.tgz",
"integrity": "sha512-LQKXR5jyI+G/+5OhZCi40m0ArY4j46g7Tl71Vtn10Ekt5TiyDzZAoqXOCS6edQpEuGbdFgSDJxleFqLxACpKJg==",
"requires": {
"concat-stream": "1.5.2",
"docker-modem": "1.0.5",
"docker-modem": "1.0.4",
"tar-fs": "1.12.0"
},
"dependencies": {
@@ -1806,9 +1840,9 @@
"integrity": "sha1-vXWcNXqqcDZeAclrewvsCKbg2Uk="
},
"dtrace-provider": {
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.6.tgz",
"integrity": "sha1-QooiOv4DQl0s1tY0f99AxmkDVj0=",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.7.1.tgz",
"integrity": "sha1-wGswjy8Q1dWDiuycVx5dWI3HHQQ=",
"optional": true,
"requires": {
"nan": "2.8.0"
@@ -1850,9 +1884,9 @@
}
},
"duplexify": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.3.tgz",
"integrity": "sha512-g8ID9OroF9hKt2POf8YLayy+9594PzmM3scI00/uBXocX3TWNgoB67hjzkFe9ITAbQOne/lLdBxHXvYUM4ZgGA==",
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.1.tgz",
"integrity": "sha512-j5goxHTwVED1Fpe5hh3q9R93Kip0Bg2KVAt4f8CEYM3UEwYcPSvWbXaUQOzdX/HtiNomipv+gU7ASQPDbV7pGQ==",
"requires": {
"end-of-stream": "1.4.0",
"inherits": "2.0.3",
@@ -2450,14 +2484,6 @@
"integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=",
"dev": true
},
"follow-redirects": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz",
"integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==",
"requires": {
"debug": "3.1.0"
}
},
"for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -2606,24 +2632,23 @@
}
},
"gcp-metadata": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.6.1.tgz",
"integrity": "sha512-Ju3brkV7kYOBP5s3Z6HS2xd7gyH9MDfuKeB+y51SsI8GPrD37NOB5Re9fWXQQVAkd74zzVOScnNic1lcRsWD9w==",
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.3.1.tgz",
"integrity": "sha512-5kJPX/RXuqoLmHiOOgkSDk/LI0QaXpEvZ3pvQP4ifjGGDKZKVSOjL/GcDjXA5kLxppFCOjmmsu0Uoop9d1upaQ==",
"requires": {
"axios": "0.17.1",
"extend": "3.0.1",
"retry-axios": "0.3.0"
"retry-request": "3.3.1"
}
},
"gcs-resumable-upload": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-0.9.0.tgz",
"integrity": "sha512-+Zrmr0JKO2y/2mg953TW6JLu+NAMHqQsKzqCm7CIT24gMQakolPJCMzDleVpVjXAqB7ZCD276tcUq2ebOfqTug==",
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-0.8.2.tgz",
"integrity": "sha512-PBl1OFABYxubxfYPh000I0+JLbQzBRtNqxzgxYboIQk2tdw7BvjJ2dVukk3YH4QM6GiUwqItyNqWBuxjLH8GhA==",
"requires": {
"buffer-equal": "1.0.0",
"configstore": "3.1.1",
"google-auto-auth": "0.9.4",
"pumpify": "1.4.0",
"google-auto-auth": "0.7.2",
"pumpify": "1.3.5",
"request": "2.83.0",
"stream-events": "1.0.2",
"through2": "2.0.3"
@@ -2868,25 +2893,24 @@
}
},
"google-auth-library": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.12.0.tgz",
"integrity": "sha512-79qCXtJ1VweBmmLr4yLq9S4clZB2p5Y+iACvuKk9gu4JitEnPc+bQFmYvtCYehVR44MQzD1J8DVmYW2w677IEw==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.10.0.tgz",
"integrity": "sha1-bhW6vuhf0d0U2NEoopW2g41SE24=",
"requires": {
"gtoken": "1.2.3",
"jws": "3.1.4",
"lodash.isstring": "4.0.1",
"lodash.merge": "4.6.1",
"lodash.noop": "3.0.1",
"request": "2.83.0"
}
},
"google-auto-auth": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.9.4.tgz",
"integrity": "sha512-a/gSNZ2RCaJxriBO/A010IHmdiQeoZS0EE83G7R/yV/OGXM9zd3otRqlcfRUomBLXf9XgsJ0h6bCp7bo+qaPvw==",
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.7.2.tgz",
"integrity": "sha512-ux2n2AE2g3+vcLXwL4dP/M12SFMRX5dzCzBfhAEkTeAB7dpyGdOIEj7nmUx0BHKaCcUQrRWg9kT63X/Mmtk1+A==",
"requires": {
"async": "2.6.0",
"gcp-metadata": "0.6.1",
"google-auth-library": "0.12.0",
"gcp-metadata": "0.3.1",
"google-auth-library": "0.10.0",
"request": "2.83.0"
}
},
@@ -3594,7 +3618,8 @@
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"dev": true
},
"is-builtin-module": {
"version": "1.0.0",
@@ -3974,9 +3999,9 @@
"dev": true
},
"jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz",
"integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ="
},
"jsonpointer": {
"version": "4.0.1",
@@ -4060,16 +4085,16 @@
}
},
"ldapjs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz",
"integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.1.tgz",
"integrity": "sha1-NSuBKudLCo6WVJpLiWBg7uG5pUY=",
"requires": {
"asn1": "0.2.3",
"assert-plus": "1.0.0",
"backoff": "2.5.0",
"bunyan": "1.8.12",
"dashdash": "1.14.1",
"dtrace-provider": "0.8.6",
"dtrace-provider": "0.7.1",
"ldap-filter": "0.2.2",
"once": "1.4.0",
"vasync": "1.6.4",
@@ -4266,9 +4291,9 @@
"dev": true
},
"lodash.merge": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz",
"integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ=="
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz",
"integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU="
},
"lodash.mergewith": {
"version": "4.6.0",
@@ -4276,6 +4301,11 @@
"integrity": "sha1-FQzwoWeR9ZA7iJHqsVRgknS96lU=",
"dev": true
},
"lodash.noop": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-3.0.1.tgz",
"integrity": "sha1-OBiPTWUKOkdCWEObluxFsyYXEzw="
},
"lodash.restparam": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
@@ -4361,9 +4391,9 @@
"dev": true
},
"make-dir": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz",
"integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.1.0.tgz",
"integrity": "sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==",
"requires": {
"pify": "3.0.0"
}
@@ -4440,7 +4470,7 @@
"resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
"integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
"requires": {
"mimic-fn": "1.2.0"
"mimic-fn": "1.1.0"
}
},
"memoizee": {
@@ -4522,9 +4552,9 @@
}
},
"mime": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.2.0.tgz",
"integrity": "sha512-0Qz9uF1ATtl8RKJG4VRfOymh7PyEor6NbrI/61lRfuRe4vx9SNATrvAeTj2EWVRKjEQGskrzWkJBBY5NbaVHIA=="
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.0.3.tgz",
"integrity": "sha512-TrpAd/vX3xaLPDgVRm6JkZwLR0KHfukMdU2wTEbqMDdCnY6Yo3mE+mjs9YE6oMNw2QRfXVeBEYpmpO94BIqiug=="
},
"mime-db": {
"version": "1.30.0",
@@ -4540,9 +4570,9 @@
}
},
"mimic-fn": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz",
"integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg="
},
"minimatch": {
"version": "3.0.4",
@@ -4587,9 +4617,9 @@
}
},
"mocha": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.1.tgz",
"integrity": "sha512-SpwyojlnE/WRBNGtvJSNfllfm5PqEDFxcWluSIgLeSBJtXG4DmoX2NNAeEA7rP5kK+79VgtVq8nG6HskaL1ykg==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-4.0.1.tgz",
"integrity": "sha512-evDmhkoA+cBNiQQQdSKZa2b9+W2mpLoj50367lhy+Klnx9OV8XlCIhigUnn1gaTFLQCa0kdNhEGDr0hCXOQFDw==",
"dev": true,
"requires": {
"browser-stdout": "1.3.0",
@@ -4630,9 +4660,9 @@
}
},
"modelo": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.3.tgz",
"integrity": "sha512-9DITV2YEMcw7XojdfvGl3gDD8J9QjZTJ7ZOUuSAkP+F3T6rDbzMJuPktxptsdHYEvZcmXrCD3LMOhdSAEq6zKA=="
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.0.tgz",
"integrity": "sha1-O0tCACOmbKfjK9uhbnEJN+FNGws="
},
"moment": {
"version": "2.19.4",
@@ -4821,7 +4851,7 @@
"mkdirp": "0.5.1",
"propagate": "0.4.0",
"qs": "6.5.1",
"semver": "5.5.0"
"semver": "5.4.1"
},
"dependencies": {
"debug": {
@@ -5056,7 +5086,7 @@
"stringstream": "0.0.5",
"tough-cookie": "2.3.3",
"tunnel-agent": "0.4.3",
"uuid": "3.2.1"
"uuid": "3.1.0"
}
},
"sntp": {
@@ -5077,9 +5107,9 @@
}
},
"nodemailer": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.0.tgz",
"integrity": "sha512-yYTtNAmukmu0qI/zAYGggOvOdMgwacp4jmvCyXrGao/84Elz+2lDGOhVg6bkvOhkRK/hnun3XoJ+AV7w8x4wbQ=="
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.4.1.tgz",
"integrity": "sha512-1bnszJJXatcHJhLpxQ1XMkLDjCjPKvGKMtRQ73FOsoNln3UQjddEQmz6fAwM3aj0GtQ3dQX9qtMHPelz63GU7A=="
},
"nodemailer-fetch": {
"version": "1.6.0",
@@ -5126,7 +5156,7 @@
"requires": {
"hosted-git-info": "2.5.0",
"is-builtin-module": "1.0.0",
"semver": "5.5.0",
"semver": "5.4.1",
"validate-npm-package-license": "3.0.1"
}
},
@@ -5475,26 +5505,18 @@
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
},
"p-limit": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz",
"integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==",
"requires": {
"p-try": "1.0.0"
}
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz",
"integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw="
},
"p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"requires": {
"p-limit": "1.2.0"
"p-limit": "1.1.0"
}
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
},
"parse-database-url": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/parse-database-url/-/parse-database-url-0.3.0.tgz",
@@ -6856,24 +6878,13 @@
}
},
"pumpify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.4.0.tgz",
"integrity": "sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA==",
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.3.5.tgz",
"integrity": "sha1-G2ccYZlAq8rqwK0OOjwWS+dgmTs=",
"requires": {
"duplexify": "3.5.3",
"duplexify": "3.5.1",
"inherits": "2.0.3",
"pump": "2.0.1"
},
"dependencies": {
"pump": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
"requires": {
"end-of-stream": "1.4.0",
"once": "1.4.0"
}
}
"pump": "1.0.3"
}
},
"punycode": {
@@ -6929,9 +6940,9 @@
}
},
"rc": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.5.tgz",
"integrity": "sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0=",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.2.tgz",
"integrity": "sha1-2M6ctX6NZNnHut2YdsfDTL48cHc=",
"requires": {
"deep-extend": "0.4.2",
"ini": "1.3.5",
@@ -7146,7 +7157,7 @@
"stringstream": "0.0.5",
"tough-cookie": "2.3.3",
"tunnel-agent": "0.6.0",
"uuid": "3.2.1"
"uuid": "3.1.0"
}
},
"require-directory": {
@@ -7183,11 +7194,6 @@
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"dev": true
},
"retry-axios": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.0.tgz",
"integrity": "sha512-6vOCghodB5p5N/ZOqug7A3WsT42TULZ7NErUi4lP3KtwtXgz4hE/43LWHsFuHuBfXRmOm/tjXBWAjnObrcy+yg=="
},
"retry-request": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-3.3.1.tgz",
@@ -7236,9 +7242,9 @@
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"safe-json-stringify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz",
"integrity": "sha512-EzBtUaFH9bHYPc69wqjp0efJI/DPNHdFbGE3uIMn4sVbO0zx8vZ8cG4WKxQfOpUOKsQyGBiT2mTqnCw+6nLswA==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz",
"integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=",
"optional": true
},
"safetydance": {
@@ -7349,9 +7355,9 @@
}
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
},
"send": {
"version": "0.16.1",
@@ -7465,7 +7471,7 @@
"resolved": "https://registry.npmjs.org/showdown/-/showdown-1.8.5.tgz",
"integrity": "sha1-QYOQzFvS0Y3ql5uaGyKhVqep7Nc=",
"requires": {
"yargs": "10.1.2"
"yargs": "10.0.3"
}
},
"sigmund": {
@@ -7726,7 +7732,7 @@
"integrity": "sha1-URGNFUVV31Rp7h9n4M8efoosDjo=",
"requires": {
"asn1": "0.2.3",
"semver": "5.5.0",
"semver": "5.4.1",
"streamsearch": "0.1.2"
}
},
@@ -7833,9 +7839,9 @@
"dev": true
},
"string-format-obj": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.1.tgz",
"integrity": "sha512-Mm+sROy+pHJmx0P/0Bs1uxIX6UhGJGj6xDGQZ5zh9v/SZRmLGevp+p0VJxV7lirrkAmQ2mvva/gHKpnF/pTb+Q=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.0.tgz",
"integrity": "sha1-djVhCx7zlwE+hHi+mKFw4EmD0Gg="
},
"string-width": {
"version": "2.1.1",
@@ -8075,9 +8081,9 @@
}
},
"tldjs": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.3.1.tgz",
"integrity": "sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.2.0.tgz",
"integrity": "sha512-5b5t+HKprfccAFRAsH/fzDR4O+UgO6vStvbaJo10jvMcUavlwxR3Jrn2WmXfjG3k22T7b4pqqfput38nr1RpJQ==",
"requires": {
"punycode": "1.4.1"
}
@@ -8525,9 +8531,9 @@
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA=="
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
},
"v8flags": {
"version": "2.1.1",
@@ -8554,9 +8560,9 @@
}
},
"validator": {
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz",
"integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA=="
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-9.2.0.tgz",
"integrity": "sha512-6Ij4Eo0KM4LkR0d0IegOwluG5453uqT5QyF5SV5Ezvm8/zmkKI/L4eoraafZGlZPC9guLkwKzgypcw8VGWWnGA=="
},
"vary": {
"version": "1.1.2",
@@ -8825,9 +8831,9 @@
}
},
"ws": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.2.tgz",
"integrity": "sha512-t+WGpsNxhMR4v6EClXS8r8km5ZljKJzyGhJf7goJz9k5Ye3+b5Bvno5rjqPuIBn5mnn5GBb7o8IrIWHxX1qOLQ==",
"requires": {
"async-limiter": "1.0.0",
"safe-buffer": "5.1.1",
@@ -8878,11 +8884,11 @@
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
},
"yargs": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-10.1.2.tgz",
"integrity": "sha512-ivSoxqBGYOqQVruxD35+EyCFDYNEFL/Uo6FcOnz+9xZdZzK0Zzw4r4KhbrME1Oo2gOggwJod2MnsdamSG7H9ig==",
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-10.0.3.tgz",
"integrity": "sha512-DqBpQ8NAUX4GyPP/ijDGHsJya4tYqLQrjPr95HNsr1YwL3+daCfvBwg7+gIC6IdJhR2kATh3hb61vjzMWEtjdw==",
"requires": {
"cliui": "4.0.0",
"cliui": "3.2.0",
"decamelize": "1.2.0",
"find-up": "2.1.0",
"get-caller-file": "1.0.2",
@@ -8893,40 +8899,7 @@
"string-width": "2.1.1",
"which-module": "2.0.0",
"y18n": "3.2.1",
"yargs-parser": "8.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
},
"cliui": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-4.0.0.tgz",
"integrity": "sha512-nY3W5Gu2racvdDk//ELReY+dHjb9PlIcVDFXP72nVIhq2Gy3LuVXYwJoPVudwQnv1shtohpgkdCKT2YaKY0CKw==",
"requires": {
"string-width": "2.1.1",
"strip-ansi": "4.0.0",
"wrap-ansi": "2.1.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"requires": {
"ansi-regex": "3.0.0"
}
},
"yargs-parser": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz",
"integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==",
"requires": {
"camelcase": "4.1.0"
}
}
"yargs-parser": "8.0.0"
}
},
"yargs-parser": {
+15 -15
View File
@@ -14,11 +14,11 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^0.7.1",
"@google-cloud/storage": "^1.6.0",
"@google-cloud/dns": "^0.7.0",
"@google-cloud/storage": "^1.2.1",
"@sindresorhus/df": "^2.1.0",
"async": "^2.6.0",
"aws-sdk": "^2.201.0",
"aws-sdk": "^2.151.0",
"body-parser": "^1.18.2",
"cloudron-manifestformat": "^2.11.0",
"connect-ensure-login": "^0.1.1",
@@ -28,24 +28,24 @@
"cookie-session": "^1.3.2",
"cron": "^1.3.0",
"csurf": "^1.6.6",
"db-migrate": "^0.10.5",
"db-migrate": "^0.10.0-beta.24",
"db-migrate-mysql": "^1.1.10",
"debug": "^3.1.0",
"dockerode": "^2.5.4",
"dockerode": "^2.5.3",
"ejs": "^2.5.7",
"ejs-cli": "^2.0.0",
"express": "^4.16.2",
"express-session": "^1.15.6",
"hat": "0.0.3",
"json": "^9.0.3",
"ldapjs": "^1.0.2",
"ldapjs": "^1.0.0",
"lodash.chunk": "^4.2.0",
"mime": "^2.2.0",
"mime": "^2.0.3",
"moment-timezone": "^0.5.14",
"morgan": "^1.9.0",
"multiparty": "^4.1.2",
"mysql": "^2.15.0",
"nodemailer": "^4.6.0",
"nodemailer": "^4.4.0",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.3.2",
@@ -62,19 +62,19 @@
"request": "^2.83.0",
"s3-block-read-stream": "^0.2.0",
"safetydance": "^0.7.1",
"semver": "^5.5.0",
"semver": "^5.4.1",
"showdown": "^1.8.2",
"split": "^1.0.0",
"superagent": "^3.8.1",
"supererror": "^0.7.1",
"tar-fs": "^1.16.0",
"tar-stream": "^1.5.5",
"tldjs": "^2.3.1",
"tldjs": "^2.2.0",
"underscore": "^1.7.0",
"uuid": "^3.2.1",
"uuid": "^3.1.0",
"valid-url": "^1.0.9",
"validator": "^9.4.1",
"ws": "^3.3.3"
"validator": "^9.1.1",
"ws": "^3.3.1"
},
"devDependencies": {
"bootstrap-sass": "^3.3.3",
@@ -91,12 +91,12 @@
"hock": "^1.3.2",
"istanbul": "*",
"js2xmlparser": "^3.0.0",
"mocha": "^5.0.1",
"mocha": "*",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^9.0.14",
"node-sass": "^4.6.1",
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
"yargs": "^10.1.2"
"yargs": "^10.0.3"
},
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
+3 -7
View File
@@ -26,10 +26,6 @@ readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly disk_size_bytes=$(LC_ALL=C df --output=size / | tail -n1)
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024))
readonly RED='\033[31m'
readonly GREEN='\033[32m'
readonly DONE='\033[m'
# verify the system has minimum requirements met
if [[ "${rootfs_type}" != "ext4" ]]; then
echo "Error: Cloudron requires '/' to be ext4" # see #364
@@ -124,7 +120,7 @@ echo ""
echo " Follow setup logs in a second terminal with:"
echo " $ tail -f ${LOG_FILE}"
echo ""
echo " Join us at https://forum.cloudron.io for any questions."
echo " Join us at https://chat.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
@@ -203,10 +199,10 @@ while true; do
sleep 10
done
echo -e "\n\n${GREEN}Visit https://<IP> to finish setup once the server has rebooted.${DONE}"
echo -e "\n\nVisit https://<IP> to finish setup once the server has rebooted.\n"
if [[ "${rebootServer}" == "true" ]]; then
echo -e "\nRebooting this server now to let changes take effect.\n"
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
systemctl reboot
fi
+1 -5
View File
@@ -41,11 +41,7 @@ fi
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
branch=$(git rev-parse --abbrev-ref HEAD)
if [[ "${branch}" == "master" ]]; then
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git rev-parse "${branch}")
else
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git fetch && git rev-parse "origin/${branch}")
fi
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git fetch && git rev-parse "origin/${branch}")
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${webadmin_version:0:10}.tar.gz"
+18 -53
View File
@@ -28,7 +28,6 @@ var appdb = require('./appdb.js'),
generatePassword = require('password-generator'),
hat = require('hat'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
once = require('once'),
path = require('path'),
@@ -113,9 +112,10 @@ var KNOWN_ADDONS = {
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
function debugApp(app, args) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? app.intrinsicFqdn : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function setupAddons(app, addons, callback) {
@@ -250,7 +250,7 @@ function setupOauth(app, options, callback) {
if (!app.sso) return callback(null);
var appId = app.id;
var redirectURI = 'https://' + app.fqdn;
var redirectURI = 'https://' + (app.altDomain || app.intrinsicFqdn);
var scope = 'profile';
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
@@ -291,27 +291,20 @@ function setupEmail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
mail.getAll(function (error, mailDomains) {
if (error) return callback(error);
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
debugApp(app, 'Setting up Email');
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: app.domain },
{ name: 'MAIL_DOMAINS', value: mailInDomains }
];
debugApp(app, 'Setting up Email');
appdb.setAddonConfig(app.id, 'email', env, callback);
});
appdb.setAddonConfig(app.id, 'email', env, callback);
}
function teardownEmail(app, options, callback) {
@@ -467,10 +460,6 @@ function teardownMySql(app, options, callback) {
}
function backupMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up mysql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -484,10 +473,6 @@ function backupMySql(app, options, callback) {
}
function restoreMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMySql(app, options, function (error) {
@@ -540,10 +525,6 @@ function teardownPostgreSql(app, options, callback) {
}
function backupPostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up postgresql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -557,10 +538,6 @@ function backupPostgreSql(app, options, callback) {
}
function restorePostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback);
setupPostgreSql(app, options, function (error) {
@@ -614,10 +591,6 @@ function teardownMongoDb(app, options, callback) {
}
function backupMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up mongodb');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -631,10 +604,6 @@ function backupMongoDb(app, options, callback) {
}
function restoreMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMongoDb(app, options, function (error) {
@@ -678,7 +647,7 @@ function setupRedis(app, options, callback) {
}
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
const label = app.fqdn;
const label = app.intrinsicFqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--label=location=${label} \
@@ -728,7 +697,7 @@ function teardownRedis(app, options, callback) {
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error /* ,stdout , stderr*/) {
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error, stdout, stderr) {
if (error) return callback(new Error('Error removing redis data:' + error));
appdb.unsetAddonConfig(app.id, 'redis', callback);
@@ -737,10 +706,6 @@ function teardownRedis(app, options, callback) {
}
function backupRedis(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up redis');
var cmd = [ '/addons/redis/service.sh', 'backup' ]; // the redis dir is volume mounted
+4 -5
View File
@@ -61,7 +61,7 @@ var assert = require('assert'),
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.domain', 'apps.dnsRecordId',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.altDomain', 'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -196,18 +196,17 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
var accessRestriction = data.accessRestriction || null;
var accessRestrictionJson = JSON.stringify(accessRestriction);
var memoryLimit = data.memoryLimit || 0;
var altDomain = data.altDomain || null;
var xFrameOptions = data.xFrameOptions || '';
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
var sso = 'sso' in data ? data.sso : null;
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson ]
});
Object.keys(portBindings).forEach(function (env) {
+7 -5
View File
@@ -5,6 +5,7 @@ var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
DatabaseError = require('./databaseerror.js'),
config = require('./config.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
mailer = require('./mailer.js'),
@@ -23,9 +24,13 @@ var gRunTimeout = null;
var gDockerEventStream = null;
function debugApp(app) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + app.manifest.id + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
var prefix = app ? app.intrinsicFqdn : '(no app)';
var manifestAppId = app ? app.manifest.id : '';
var id = app ? app.id : '';
debug(prefix + ' ' + manifestAppId + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
}
function setHealth(app, health, callback) {
@@ -66,9 +71,6 @@ function setHealth(app, health, callback) {
// callback is called with error for fatal errors and not if health check failed
function checkAppHealth(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
return callback(null);
+78 -71
View File
@@ -306,19 +306,17 @@ function getDuplicateErrorDetails(location, portBindings, error) {
return new AppsError(AppsError.ALREADY_EXISTS);
}
// app configs that is useful for 'archival' into the app backup config.json
function getAppConfig(app) {
return {
manifest: app.manifest,
location: app.location,
domain: app.domain,
fqdn: app.fqdn,
intrinsicFqdn: app.intrinsicFqdn,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
robotsTxt: app.robotsTxt,
sso: app.sso
altDomain: app.altDomain
};
}
@@ -364,8 +362,10 @@ function get(appId, callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
callback(null, app);
});
@@ -386,8 +386,10 @@ function getByIpAddress(ip, callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
callback(null, app);
});
@@ -405,8 +407,10 @@ function getAll(callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
iteratorDone();
});
@@ -464,6 +468,7 @@ function install(data, auditSource, callback) {
cert = data.cert || null,
key = data.key || null,
memoryLimit = data.memoryLimit || 0,
altDomain = data.altDomain || null,
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
sso = 'sso' in data ? data.sso : null,
debugMode = data.debugMode || null,
@@ -508,6 +513,8 @@ function install(data, auditSource, callback) {
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
var appId = uuid.v4();
if (icon) {
@@ -522,13 +529,13 @@ function install(data, auditSource, callback) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var fqdn = domains.fqdn(location, domain, domainObject.provider);
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, fqdn);
error = validateHostname(location, domain, intrinsicFqdn);
if (error) return callback(error);
if (cert && key) {
error = reverseProxy.validateCertificate(fqdn, cert, key);
error = reverseProxy.validateCertificate(intrinsicFqdn, cert, key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
@@ -543,36 +550,32 @@ function install(data, auditSource, callback) {
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
altDomain: altDomain,
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
robotsTxt: robotsTxt
robotsTxt: robotsTxt,
intrinsicFqdn: intrinsicFqdn
};
appdb.add(appId, appStoreId, manifest, location, domain, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// save cert to boxdata/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
taskmanager.restartAppTask(appId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, domain: domain, manifest: manifest, backupId: backupId });
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, app: result });
callback(null, { id : appId });
});
callback(null, { id : appId });
});
});
});
@@ -585,8 +588,9 @@ function configure(appId, data, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var domain, location, portBindings, values = { };
if ('location' in data) location = values.location = data.location.toLowerCase();
@@ -601,6 +605,11 @@ function configure(appId, data, auditSource, callback) {
if (error) return callback(error);
}
if ('altDomain' in data) {
values.altDomain = data.altDomain;
if (values.altDomain !== null && !validator.isFQDN(values.altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
}
if ('portBindings' in data) {
portBindings = values.portBindings = data.portBindings;
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
@@ -637,22 +646,24 @@ function configure(appId, data, auditSource, callback) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var fqdn = domains.fqdn(location, domain, domainObject.provider);
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, fqdn);
error = validateHostname(location, domain, intrinsicFqdn);
if (error) return callback(error);
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
var vhost = values.altDomain || intrinsicFqdn;
error = reverseProxy.validateCertificate(vhost, data.cert, data.key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
} else { // remove existing cert/key
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}..user.key`))) debug('Error removing key: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${vhost}..user.key`))) debug('Error removing key: ' + safe.error.message);
}
}
@@ -676,14 +687,9 @@ function configure(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
callback(null);
});
callback(null);
});
});
});
@@ -723,8 +729,9 @@ function update(appId, data, auditSource, callback) {
}
}
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
@@ -749,7 +756,7 @@ function update(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app });
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force });
// clear update indicator, if update fails, it will come back through the update checker
updateChecker.resetAppUpdateInfo(appId);
@@ -773,8 +780,10 @@ function getLogs(appId, options, callback) {
debug('Getting logs for %s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var lines = options.lines || 100,
follow = !!options.follow,
@@ -818,8 +827,9 @@ function restore(appId, data, auditSource, callback) {
debug('Will restore app with id:%s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// for empty or null backupId, use existing manifest to mimic a reinstall
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
@@ -848,7 +858,7 @@ function restore(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId, app: app });
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
callback(null);
});
@@ -874,8 +884,9 @@ function clone(appId, data, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof portBindings, 'object');
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
backups.get(backupId, function (error, backupInfo) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
@@ -895,7 +906,9 @@ function clone(appId, data, auditSource, callback) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
error = validateHostname(location, domain, domains.fqdn(location, domain, domainObject.provider));
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, intrinsicFqdn);
if (error) return callback(error);
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
@@ -913,9 +926,7 @@ function clone(appId, data, auditSource, callback) {
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, portBindings, data, function (error) {
@@ -924,14 +935,9 @@ function clone(appId, data, auditSource, callback) {
taskmanager.restartAppTask(newAppId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: result });
callback(null, { id : newAppId });
});
callback(null, { id : newAppId });
});
});
});
@@ -946,10 +952,10 @@ function uninstall(appId, auditSource, callback) {
debug('Will uninstall app with id:%s', appId);
get(appId, function (error, app) {
get(appId, function (error, result) {
if (error) return callback(error);
appstore.unpurchase(appId, app.appStoreId, function (error) {
appstore.unpurchase(appId, result.appStoreId, function (error) {
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
@@ -960,7 +966,7 @@ function uninstall(appId, auditSource, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: app });
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
taskmanager.startAppTask(appId, callback);
});
@@ -1011,7 +1017,7 @@ function checkManifestConstraints(manifest) {
}
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.version())) {
return new AppsError(AppsError.BAD_FIELD, 'App version requires a new platform version');
return new AppsError(AppsError.BAD_FIELD, 'minBoxVersion exceeds Box version');
}
return null;
@@ -1025,8 +1031,9 @@ function exec(appId, options, callback) {
var cmd = options.cmd || [ '/bin/bash' ];
assert(util.isArray(cmd) && cmd.length > 0);
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
@@ -1165,17 +1172,17 @@ function listBackups(page, perPage, appId, callback) {
function restoreInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug('marking %s for restore', app.intrinsicFqdn);
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null;
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
if (error) debug('did not mark %s for restore', app.intrinsicFqdn, error);
iteratorDone(); // always succeed
});
@@ -1187,14 +1194,14 @@ function restoreInstalledApps(callback) {
function configureInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug(`marking ${app.fqdn} for reconfigure`);
debug('marking %s for reconfigure', app.intrinsicFqdn);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, function (error) {
if (error) debug(`Error marking ${app.fqdn} for reconfigure: ${JSON.stringify(error)}`);
if (error) debug('did not mark %s for reconfigure', app.intrinsicFqdn, error);
iteratorDone(); // always succeed
});
+56 -97
View File
@@ -18,12 +18,9 @@ exports = module.exports = {
AppstoreError: AppstoreError
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
os = require('os'),
@@ -130,7 +127,7 @@ function unpurchase(appId, appstoreId, callback) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 404) return callback(null); // was never purchased
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
@@ -143,87 +140,62 @@ function unpurchase(appId, appstoreId, callback) {
});
}
function sendAliveStatus(callback) {
function sendAliveStatus(data, callback) {
callback = callback || NOOP_CALLBACK;
var allSettings, allDomains, mailDomains, loginEvents;
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
async.series([
function (callback) {
settings.getAll(function (error, result) {
mail.getAll(function (error, mailDomains) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allSettings = result;
callback();
});
},
function (callback) {
domains.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allDomains = result;
callback();
});
},
function (callback) {
mail.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
mailDomains = result;
callback();
});
},
function (callback) {
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
loginEvents = result;
callback();
});
}
], function (error) {
if (error) return callback(error);
var backendSettings = {
backupConfig: {
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: allDomains.length,
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
};
var backendSettings = {
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: mailDomains.length
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY],
};
var data = {
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
var data = {
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
callback(null);
});
});
});
});
});
@@ -270,12 +242,9 @@ function getAppUpdate(app, callback) {
const updateInfo = result.body;
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(app.manifest.version, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, app.manifest.version, updateInfo);
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
}
@@ -312,26 +281,16 @@ function sendFeedback(info, callback) {
assert.strictEqual(typeof info.description, 'string');
assert.strictEqual(typeof callback, 'function');
function collectAppInfoIfNeeded(callback) {
if (!info.appId) return callback();
apps.get(info.appId, callback);
}
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
collectAppInfoIfNeeded(function (error, result) {
if (error) console.error('Unable to get app info', error);
if (result) info.app = result;
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
callback(null);
});
callback(null);
});
});
}
+34 -9
View File
@@ -15,7 +15,8 @@ exports = module.exports = {
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
_waitForDnsPropagation: waitForDnsPropagation
_waitForDnsPropagation: waitForDnsPropagation,
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
};
require('supererror')({ splatchError: true });
@@ -70,7 +71,8 @@ function initialize(callback) {
function debugApp(app) {
assert.strictEqual(typeof app, 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? (app.intrinsicFqdn || '(bare)') : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
// updates the app object and the database
@@ -264,16 +266,16 @@ function registerSubdomain(app, overwrite, callback) {
if (error) return callback(error);
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.fqdn, overwrite);
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.intrinsicFqdn, overwrite);
// get the current record before updating it
domains.getDnsRecords(app.location, app.domain, 'A', function (error, values) {
domains.getDNSRecords(app.location, app.domain, 'A', function (error, values) {
if (error) return retryCallback(error);
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId);
@@ -303,9 +305,9 @@ function unregisterSubdomain(app, location, domain, callback) {
if (error) return callback(error);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', app.fqdn);
debugApp(app, 'Unregistering subdomain: %s', app.intrinsicFqdn);
domains.removeDnsRecords(location, domain, 'A', [ ip ], function (error) {
domains.removeDNSRecords(location, domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
@@ -341,10 +343,27 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 120 }, callback);
domains.waitForDNSRecord(app.intrinsicFqdn, app.domain, ip, 'A', { interval: 5000, times: 120 }, callback);
});
}
function waitForAltDomainDnsPropagation(app, callback) {
if (!app.altDomain) return callback(null);
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
// an app has an external domain and cloudron is migrated to custom domain.
var isNakedDomain = tld.getDomain(app.altDomain) === app.altDomain;
if (isNakedDomain) { // check naked domains with A record since CNAME records don't work there
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), ip, 'A', { interval: 10000, times: 60 }, callback);
});
} else {
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), app.intrinsicFqdn + '.', 'CNAME', { interval: 10000, times: 60 }, callback);
}
}
// Ordering is based on the following rationale:
// - configure nginx, icon, oauth
// - register subdomain.
@@ -427,6 +446,9 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig
updateApp.bind(null, app, { installationProgress: '95, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
@@ -472,7 +494,7 @@ function configure(app, callback) {
assert.strictEqual(typeof callback, 'function');
// oldConfig can be null during an infra update
var locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
var locationChanged = app.oldConfig && (app.oldConfig.intrinsicFqdn !== app.intrinsicFqdn);
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
@@ -519,6 +541,9 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
+16 -19
View File
@@ -68,9 +68,10 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js');
function debugApp(app) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? app.intrinsicFqdn : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function BackupsError(reason, errorOrMessage) {
@@ -231,11 +232,6 @@ function sync(backupConfig, backupId, dataDir, callback) {
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
function setBackupProgress(message) {
debug(message);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, message);
}
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
debug('sync: processing task: %j', task);
var backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), task.path);
@@ -243,23 +239,24 @@ function sync(backupConfig, backupId, dataDir, callback) {
if (task.operation === 'removedir') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing directory ${task.path}`);
return api(backupConfig.provider).removeDir(backupConfig, backupFilePath)
.on('progress', setBackupProgress)
.on('progress', function (detail) {
debug(`sync: ${detail}`);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, detail);
})
.on('done', iteratorCallback);
} else if (task.operation === 'remove') {
setBackupProgress(`Removing ${task.path}`);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing ${task.path}`);
return api(backupConfig.provider).remove(backupConfig, backupFilePath, iteratorCallback);
}
var retryCount = 0;
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
++retryCount;
debug(`${task.operation} ${task.path} try ${retryCount}`);
if (task.operation === 'add') {
setBackupProgress(`Adding ${task.path}`);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Adding ${task.path}`);
var stream = fs.createReadStream(path.join(dataDir, task.path));
stream.on('error', function (error) { setBackupProgress(`read stream error for ${task.path}: ${error.message}`); retryCallback(); }); // ignore error if file disappears
stream.on('error', function () { return retryCallback(); }); // ignore error if file disappears
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, retryCallback);
}
}, iteratorCallback);
@@ -297,6 +294,8 @@ function upload(backupId, format, dataDir, callback) {
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
callback = once(callback);
debug('upload: id %s format %s dataDir %s', backupId, format, dataDir);
settings.getBackupConfig(function (error, backupConfig) {
@@ -304,8 +303,6 @@ function upload(backupId, format, dataDir, callback) {
if (format === 'tgz') {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
tarStream.on('error', retryCallback); // already returns BackupsError
@@ -723,7 +720,7 @@ function backupApp(app, callback) {
const timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
progress.set(progress.BACKUP, 10, 'Backing up ' + app.fqdn);
progress.set(progress.BACKUP, 10, 'Backing up ' + (app.altDomain || app.intrinsicFqdn));
backupAppWithTimestamp(app, timestamp, function (error) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
@@ -750,12 +747,12 @@ function backupBoxAndApps(auditSource, callback) {
var step = 100/(allApps.length+2);
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
progress.set(progress.BACKUP, step * processed, 'Backing up ' + app.fqdn);
progress.set(progress.BACKUP, step * processed, 'Backing up ' + (app.altDomain || app.intrinsicFqdn));
++processed;
if (!app.enableBackup) {
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + app.fqdn);
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || app.intrinsicFqdn));
return iteratorCallback(null, null); // nothing to backup
}
@@ -765,7 +762,7 @@ function backupBoxAndApps(auditSource, callback) {
return iteratorCallback(error);
}
progress.set(progress.BACKUP, step * processed, 'Backed up ' + app.fqdn);
progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || app.intrinsicFqdn));
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
+1 -1
View File
@@ -191,7 +191,7 @@ function getAll(callback) {
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
record.domain = result.fqdn;
record.domain = result.altDomain || result.intrinsicFqdn;
tmp.push(record);
+1 -1
View File
@@ -294,7 +294,7 @@ function doUpdate(boxUpdateInfo, callback) {
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
progress.set(progress.UPDATE, 5, 'Downloading and installing new version');
progress.set(progress.UPDATE, 5, 'Downloading and extracting new version');
shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) {
if (error) return updateError(error);
+11 -37
View File
@@ -22,12 +22,12 @@ var apps = require('./apps.js'),
reverseProxy = require('./reverseproxy.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
semver = require('semver'),
updateChecker = require('./updatechecker.js');
var gJobs = {
alive: null, // send periodic stats
appAutoUpdater: null,
boxAutoUpdater: null,
autoUpdater: null,
appUpdateChecker: null,
backup: null,
boxUpdateChecker: null,
@@ -78,16 +78,14 @@ function initialize(callback) {
});
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
recreateJobs(allSettings[settings.TIME_ZONE_KEY]);
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]);
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
callback();
@@ -191,51 +189,28 @@ function recreateJobs(tz) {
});
}
function boxAutoupdatePatternChanged(pattern) {
function autoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
debug('Box auto update pattern changed to %s', pattern);
debug('Auto update pattern changed to %s', pattern);
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
if (gJobs.autoUpdater) gJobs.autoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.boxAutoUpdater = new CronJob({
gJobs.autoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No box auto updates available');
}
},
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
}
function appAutoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
debug('Apps auto update pattern changed to %s', pattern);
if (gJobs.appAutoUpdater) gJobs.appAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.appAutoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.apps) {
} else if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No app auto updates available');
debug('No auto updates available');
}
},
start: true,
@@ -266,8 +241,7 @@ function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
for (var job in gJobs) {
+5 -6
View File
@@ -13,7 +13,6 @@ var assert = require('assert'),
constants = require('./constants.js'),
eventlog = require('./eventlog.js'),
tokendb = require('./tokendb.js'),
user = require('./user.js'),
util = require('util');
function DeveloperError(reason, errorOrMessage) {
@@ -38,19 +37,19 @@ util.inherits(DeveloperError, Error);
DeveloperError.INTERNAL_ERROR = 'Internal Error';
DeveloperError.EXTERNAL_ERROR = 'External Error';
function issueDeveloperToken(userObject, ip, callback) {
assert.strictEqual(typeof userObject, 'object');
assert.strictEqual(typeof ip, 'string');
function issueDeveloperToken(user, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var token = tokendb.generateToken();
var expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
var scopes = '*,' + clients.SCOPE_ROLE_SDK;
tokendb.add(token, userObject.id, 'cid-cli', expiresAt, scopes, function (error) {
tokendb.add(token, user.id, 'cid-cli', expiresAt, scopes, function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'cli', ip: ip }, { userId: userObject.id, user: user.removePrivateFields(userObject) });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
});
+46
View File
@@ -0,0 +1,46 @@
'use strict';
exports = module.exports = {
resolve: resolve
};
var assert = require('assert'),
child_process = require('child_process'),
debug = require('debug')('box:dig');
function resolve(domain, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// dig @server cloudron.io TXT +short
var args = [ ];
if (options.server) args.push('@' + options.server);
if (type === 'PTR') {
args.push('-x', domain);
} else {
args.push(domain, type);
}
args.push('+short');
child_process.execFile('/usr/bin/dig', args, { encoding: 'utf8', killSignal: 'SIGKILL', timeout: options.timeout || 0 }, function (error, stdout, stderr) {
if (error && error.killed) error.code = 'ETIMEDOUT';
if (error || stderr) debug('resolve error (%j): %j %s %s', args, error, stdout, stderr);
if (error) return callback(error);
debug('resolve (%j): %s', args, stdout);
if (!stdout) return callback(); // timeout or no result
var lines = stdout.trim().split('\n');
if (type === 'MX') {
lines = lines.map(function (line) {
var parts = line.split(' ');
return { priority: parts[0], exchange: parts[1] };
});
}
return callback(null, lines);
});
}
+6 -6
View File
@@ -11,7 +11,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
superagent = require('superagent'),
util = require('util'),
@@ -58,7 +58,7 @@ function getZoneByName(dnsConfig, zoneName, callback) {
});
}
function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneId, 'string');
assert.strictEqual(typeof zoneName, 'string');
@@ -100,7 +100,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
var zoneId = result.id;
getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
var dnsRecords = result;
@@ -171,7 +171,7 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.content; });
@@ -193,7 +193,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
if (error) return callback(error);
if (result.length === 0) return callback(null);
@@ -243,7 +243,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+2 -2
View File
@@ -11,7 +11,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
safe = require('safetydance'),
superagent = require('superagent'),
@@ -210,7 +210,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+2 -2
View File
@@ -10,7 +10,7 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:dns/gcdns'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
GCDNS = require('@google-cloud/dns'),
util = require('util'),
@@ -172,7 +172,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
var credentials = getDnsCredentials(dnsConfig);
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, resolvedNS) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !resolvedNS) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+2 -2
View File
@@ -10,7 +10,7 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
util = require('util');
@@ -56,7 +56,7 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof callback, 'function');
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to get nameservers'));
callback(null, { wildcard: !!dnsConfig.wildcard });
+3 -2
View File
@@ -46,10 +46,11 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function waitForDns(domain, zoneName, value, options, callback) {
function waitForDns(domain, zoneName, value, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof value, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
+4 -3
View File
@@ -13,8 +13,9 @@ exports = module.exports = {
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('../config.js'),
debug = require('debug')('box:dns/route53'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
util = require('util'),
_ = require('underscore');
@@ -192,7 +193,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
};
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error) {
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'AccessDenied') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
@@ -233,7 +234,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+44 -45
View File
@@ -5,59 +5,53 @@ exports = module.exports = waitForDns;
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/waitfordns'),
dns = require('../native-dns.js'),
DomainError = require('../domains.js').DomainError;
dig = require('../dig.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
util = require('util');
function resolveIp(hostname, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// try A record at authoritative server
debug(`resolveIp: Checking if ${hostname} has A record at ${options.server}`);
dns.resolve(hostname, 'A', options, function (error, results) {
if (!error && results.length !== 0) return callback(null, results);
// try CNAME record at authoritative server
debug(`resolveIp: Checking if ${hostname} has CNAME record at ${options.server}`);
dns.resolve(hostname, 'CNAME', options, function (error, results) {
if (error || results.length === 0) return callback(error, results);
// recurse lookup the CNAME record
debug(`resolveIp: Resolving ${hostname}'s CNAME record ${results[0]}`);
dns.resolve(results[0], 'A', { server: '127.0.0.1', timeout: options.timeout }, callback);
});
});
}
function isChangeSynced(domain, value, nameserver, callback) {
function isChangeSynced(domain, value, type, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof value, 'string');
assert(util.isRegExp(value));
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof nameserver, 'string');
assert.strictEqual(typeof callback, 'function');
// ns records cannot have cname
dns.resolve(nameserver, 'A', { timeout: 5000 }, function (error, nsIps) {
dns.resolve4(nameserver, function (error, nsIps) {
if (error || !nsIps || nsIps.length === 0) {
debug(`isChangeSynced: cannot resolve NS ${nameserver}`); // it's fine if one or more ns are dead
return callback(null, true);
debug('nameserver %s does not resolve. assuming it stays bad.', nameserver); // it's fine if one or more ns are dead
return callback(true);
}
async.every(nsIps, function (nsIp, iteratorCallback) {
resolveIp(domain, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'TIMEOUT') {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain}`);
dig.resolve(domain, type, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'ETIMEDOUT') {
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, domain);
return iteratorCallback(null, true); // should be ok if dns server is down
}
if (error) {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain}: ${error}`);
debug('nameserver %s (%s) returned error trying to resolve %s: %s', nameserver, nsIp, domain, error);
return iteratorCallback(null, false);
}
debug(`isChangeSynced: ${domain} was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}`);
if (!answer || answer.length === 0) {
debug('bad answer from nameserver %s (%s) resolving %s (%s)', nameserver, nsIp, domain, type);
return iteratorCallback(null, false);
}
iteratorCallback(null, answer.length === 1 && answer[0] === value);
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, answer, value);
var match = answer.some(function (a) {
return ((type === 'A' && value.test(a)) ||
(type === 'CNAME' && value.test(a)) ||
(type === 'TXT' && value.test(a)));
});
if (match) return iteratorCallback(null, true); // done!
iteratorCallback(null, false);
});
}, callback);
@@ -65,25 +59,30 @@ function isChangeSynced(domain, value, nameserver, callback) {
}
// check if IP change has propagated to every nameserver
function waitForDns(domain, zoneName, value, options, callback) {
function waitForDns(domain, zoneName, value, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof value, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
debug('waitForDns: domain %s to be %s in zone %s.', domain, value, zoneName);
if (typeof value === 'string') {
// http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
value = new RegExp('^' + value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$');
}
var attempt = 0;
debug('waitForIp: domain %s to be %s in zone %s.', domain, value, zoneName);
var attempt = 1;
async.retry(options, function (retryCallback) {
++attempt;
debug(`waitForDns (try ${attempt}): ${domain} to be ${value} in zone ${zoneName}`);
debug('waitForDNS: %s (zone: %s) attempt %s.', domain, zoneName, attempt++);
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new DomainError(DomainError.EXTERNAL_ERROR, 'Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, domain, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
async.every(nameservers, isChangeSynced.bind(null, domain, value, type), function (error, synced) {
debug('waitForIp: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
retryCallback(synced ? null : new DomainError(DomainError.EXTERNAL_ERROR, 'ETRYAGAIN'));
});
@@ -91,7 +90,7 @@ function waitForDns(domain, zoneName, value, options, callback) {
}, function retryDone(error) {
if (error) return callback(error);
debug(`waitForDns: ${domain} has propagated`);
debug('waitForDNS: %s done.', domain);
callback(null);
});
+5 -9
View File
@@ -15,7 +15,6 @@ exports = module.exports = {
createSubcontainer: createSubcontainer,
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
execContainer: execContainer
};
@@ -50,9 +49,10 @@ var addons = require('./addons.js'),
_ = require('underscore');
function debugApp(app, args) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? app.intrinsicFqdn : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function pullImage(manifest, callback) {
@@ -129,7 +129,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
var domain = app.altDomain || app.intrinsicFqdn;
var stdEnv = [
'CLOUDRON=1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
@@ -163,10 +163,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
memoryLimit = constants.DEFAULT_MEMORY_LIMIT;
}
// give scheduler tasks twice the memory limit since background jobs take more memory
// if required, we can make this a manifest and runtime argument later
if (!isAppContainer) memoryLimit *= 2;
// apparmor is disabled on few servers
var enableSecurityOpt = config.CLOUDRON && safe(function () { return child_process.spawnSync('aa-enabled').status === 0; }, false);
@@ -190,7 +186,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
'/run': {}
},
Labels: {
'fqdn': app.fqdn,
'fqdn': app.intrinsicFqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer)
},
+19 -15
View File
@@ -10,11 +10,11 @@ module.exports = exports = {
fqdn: fqdn,
setAdmin: setAdmin,
getDnsRecords: getDnsRecords,
upsertDnsRecords: upsertDnsRecords,
removeDnsRecords: removeDnsRecords,
getDNSRecords: getDNSRecords,
upsertDNSRecords: upsertDNSRecords,
removeDNSRecords: removeDNSRecords,
waitForDnsRecord: waitForDnsRecord,
waitForDNSRecord: waitForDNSRecord,
DomainError: DomainError
};
@@ -116,7 +116,7 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
}
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
let error = reverseProxy.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
}
@@ -164,7 +164,7 @@ function get(domain, callback) {
var cert = safe.fs.readFileSync(bundle.certFilePath, 'utf-8');
var key = safe.fs.readFileSync(bundle.keyFilePath, 'utf-8');
if (!cert || !key) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'unable to read certificates from disk'));
if (!cert || !key) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
result.fallbackCertificate = { cert: cert, key: key };
@@ -196,7 +196,7 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
let error = reverseProxy.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
}
@@ -256,7 +256,7 @@ function getName(domain, subdomain) {
return subdomain === '' ? part : subdomain + '.' + part;
}
function getDnsRecords(subdomain, domain, type, callback) {
function getDNSRecords(subdomain, domain, type, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -273,7 +273,7 @@ function getDnsRecords(subdomain, domain, type, callback) {
});
}
function upsertDnsRecords(subdomain, domain, type, values, callback) {
function upsertDNSRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -293,7 +293,7 @@ function upsertDnsRecords(subdomain, domain, type, values, callback) {
});
}
function removeDnsRecords(subdomain, domain, type, values, callback) {
function removeDNSRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -313,18 +313,22 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
});
}
// only wait for A record
function waitForDnsRecord(fqdn, domain, value, options, callback) {
function waitForDNSRecord(fqdn, domain, value, type, options, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof value, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, result) {
if (error) return callback(error);
// domain can be not found when waiting for altDomain. When we migrate altDomain, this can never happen
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
api(result.provider).waitForDns(fqdn, result ? result.zoneName : domain, value, options, callback);
// hack for lack of provider with altDomain. When we migrate altDomain, this will be automatically "manual"
const provider = result ? result.provider : 'manual';
api(provider).waitForDns(fqdn, result ? result.zoneName : domain, value, type, options, callback);
});
}
+2 -2
View File
@@ -23,7 +23,7 @@ function sync(callback) {
debug('refreshDNS: current ip %s', ip);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
domains.upsertDNSRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: done for admin location');
@@ -35,7 +35,7 @@ function sync(callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback);
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
+4 -3
View File
@@ -22,6 +22,7 @@ exports = module.exports = {
ACTION_BACKUP_START: 'backup.start',
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
ACTION_CLI_MODE: 'settings.climode',
ACTION_START: 'cloudron.start',
ACTION_UPDATE: 'cloudron.update',
ACTION_USER_ADD: 'user.add',
@@ -90,14 +91,14 @@ function get(id, callback) {
});
}
function getAllPaged(actions, search, page, perPage, callback) {
assert(Array.isArray(actions));
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
eventlogdb.getAllPaged(actions, search, page, perPage, function (error, events) {
eventlogdb.getAllPaged(action, search, page, perPage, function (error, events) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, events);
+8 -9
View File
@@ -40,8 +40,8 @@ function get(eventId, callback) {
});
}
function getAllPaged(actions, search, page, perPage, callback) {
assert(Array.isArray(actions));
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
@@ -50,15 +50,14 @@ function getAllPaged(actions, search, page, perPage, callback) {
var data = [];
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog';
if (actions.length || search) query += ' WHERE';
if (action || search) query += ' WHERE';
if (search) query += ' (source LIKE ' + mysql.escape('%' + search + '%') + ' OR data LIKE ' + mysql.escape('%' + search + '%') + ')';
if (action && search) query += ' AND ';
if (actions.length && search) query += ' AND ( ';
actions.forEach(function (action, i) {
query += ' (action LIKE ' + mysql.escape(`%${action}%`) + ') ';
if (i < actions.length-1) query += ' OR ';
});
if (actions.length && search) query += ' ) ';
if (action) {
query += ' action=?';
data.push(action);
}
query += ' ORDER BY creationTime DESC LIMIT ?,?';
+1 -1
View File
@@ -18,7 +18,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.0.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.0.1' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.2.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.1.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
}
};
+3 -5
View File
@@ -410,7 +410,7 @@ function authorizeUserForApp(req, res, next) {
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id, app: app }, { userId: req.user.id, user: user.removePrivateFields(req.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: req.user.id });
res.end();
});
@@ -418,8 +418,6 @@ function authorizeUserForApp(req, res, next) {
}
function authenticateMailbox(req, res, next) {
debug('mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
@@ -451,12 +449,12 @@ function authenticateMailbox(req, res, next) {
} else if (mailbox.ownerType === mailboxdb.TYPE_USER) {
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
user.verifyWithUsername(parts[0], req.credentials || '', function (error, result) {
user.verifyWithUsername(parts[0], req.credentials || '', function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: user.removePrivateFields(result) });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: user.username });
res.end();
});
} else {
+156 -210
View File
@@ -8,9 +8,6 @@ exports = module.exports = {
add: add,
del: del,
update: update,
addDnsRecords: addDnsRecords,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
@@ -22,7 +19,6 @@ exports = module.exports = {
sendTestMail: sendTestMail,
getMailboxes: getMailboxes,
removeMailboxes: removeMailboxes,
getUserMailbox: getUserMailbox,
enableUserMailbox: enableUserMailbox,
disableUserMailbox: disableUserMailbox,
@@ -46,7 +42,7 @@ var assert = require('assert'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:mail'),
dns = require('./native-dns.js'),
dig = require('./dig.js'),
domains = require('./domains.js'),
groups = require('./groups.js'),
GroupError = groups.GroupError,
@@ -69,7 +65,7 @@ var assert = require('assert'),
util = require('util'),
_ = require('underscore');
const DNS_OPTIONS = { server: '127.0.0.1', timeout: 5000 }; // unbound runs on 127.0.0.1
const digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function MailError(reason, errorOrMessage) {
@@ -95,7 +91,6 @@ MailError.INTERNAL_ERROR = 'Internal Error';
MailError.BAD_FIELD = 'Bad Field';
MailError.ALREADY_EXISTS = 'Already Exists';
MailError.NOT_FOUND = 'Not Found';
MailError.IN_USE = 'In Use';
function validateAlias(alias) {
assert.strictEqual(typeof alias, 'string');
@@ -123,7 +118,7 @@ function checkOutboundPort25(callback) {
'smtp.mail.yahoo.com',
'smtp.o2.ie',
'smtp.comcast.net',
'smtp.1und1.de',
'outgoing.verizon.net'
]);
var relay = {
@@ -184,11 +179,9 @@ function verifyRelay(relay, callback) {
assert.strictEqual(typeof relay, 'object');
assert.strictEqual(typeof callback, 'function');
// we used to verify cloudron-smtp with checkOutboundPort25 but that is unreliable given that we just
// randomly select some smtp server
if (relay.provider === 'cloudron-smtp') return callback();
var verifier = relay.provider === 'cloudron-smtp' ? checkOutboundPort25 : checkSmtpRelay.bind(null, relay);
checkSmtpRelay(relay, function (error) {
verifier(function (error) {
if (error) return callback(new MailError(MailError.BAD_FIELD, error.message));
callback();
@@ -207,13 +200,14 @@ function checkDkim(domain, callback) {
var dkimKey = readDkimPublicKeySync(domain);
if (!dkimKey) return callback(new Error('Failed to read dkim public key'), dkim);
dkim.expected = 'v=DKIM1; t=s; p=' + dkimKey;
dkim.expected = '"v=DKIM1; t=s; p=' + dkimKey + '"';
dns.resolve(dkim.domain, dkim.type, DNS_OPTIONS, function (error, txtRecords) {
dig.resolve(dkim.domain, dkim.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, dkim); // not setup
if (error) return callback(error, dkim);
if (txtRecords.length !== 0) {
dkim.value = txtRecords[0].join('');
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
dkim.value = txtRecords[0];
dkim.status = (dkim.value === dkim.expected);
}
@@ -226,18 +220,21 @@ function checkSpf(domain, callback) {
domain: domain,
type: 'TXT',
value: null,
expected: 'v=spf1 a:' + config.mailFqdn() + ' ~all',
expected: '"v=spf1 a:' + config.mailFqdn() + ' ~all"',
status: false
};
dns.resolve(spf.domain, spf.type, DNS_OPTIONS, function (error, txtRecords) {
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
dig.resolve(spf.domain, spf.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, spf); // not setup
if (error) return callback(error, spf);
if (!Array.isArray(txtRecords)) return callback(null, spf);
var i;
for (i = 0; i < txtRecords.length; i++) {
let txtRecord = txtRecords[i].join(''); // https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
if (txtRecord.indexOf('v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecord;
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecords[i];
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
break;
}
@@ -245,7 +242,7 @@ function checkSpf(domain, callback) {
if (spf.status) {
spf.expected = spf.value;
} else if (i !== txtRecords.length) {
spf.expected = 'v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
spf.expected = '"v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('"v=spf1 '.length);
}
callback(null, spf);
@@ -261,12 +258,13 @@ function checkMx(domain, callback) {
status: false
};
dns.resolve(mx.domain, mx.type, DNS_OPTIONS, function (error, mxRecords) {
dig.resolve(mx.domain, mx.type, digOptions, function (error, mxRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, mx); // not setup
if (error) return callback(error, mx);
if (mxRecords.length !== 0) {
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === config.mailFqdn();
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
if (Array.isArray(mxRecords) && mxRecords.length !== 0) {
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === (config.mailFqdn() + '.');
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange; }).join(' ');
}
callback(null, mx);
@@ -278,15 +276,16 @@ function checkDmarc(domain, callback) {
domain: '_dmarc.' + domain,
type: 'TXT',
value: null,
expected: 'v=DMARC1; p=reject; pct=100',
expected: '"v=DMARC1; p=reject; pct=100"',
status: false
};
dns.resolve(dmarc.domain, dmarc.type, DNS_OPTIONS, function (error, txtRecords) {
dig.resolve(dmarc.domain, dmarc.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, dmarc); // not setup
if (error) return callback(error, dmarc);
if (txtRecords.length !== 0) {
dmarc.value = txtRecords[0].join('');
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
dmarc.value = txtRecords[0];
dmarc.status = (dmarc.value === dmarc.expected);
}
@@ -299,7 +298,7 @@ function checkPtr(callback) {
domain: null,
type: 'PTR',
value: null,
expected: config.mailFqdn(), // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918)
expected: config.mailFqdn() + '.',
status: false
};
@@ -308,10 +307,11 @@ function checkPtr(callback) {
ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
dns.resolve(ptr.domain, 'PTR', DNS_OPTIONS, function (error, ptrRecords) {
dig.resolve(ip, 'PTR', digOptions, function (error, ptrRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, ptr); // not setup
if (error) return callback(error, ptr);
if (ptrRecords.length !== 0) {
if (Array.isArray(ptrRecords) && ptrRecords.length !== 0) {
ptr.value = ptrRecords.join(' ');
ptr.status = ptrRecords.some(function (v) { return v === ptr.expected; });
}
@@ -323,31 +323,15 @@ function checkPtr(callback) {
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json
const RBL_LIST = [
{
'name': 'Abuse.ch',
'dns': 'spam.abuse.ch',
'site': 'http://abuse.ch/'
},
{
'name': 'Barracuda',
'dns': 'b.barracudacentral.org',
'site': 'http://www.barracudacentral.org/rbl/removal-request'
},
{
'name': 'Composite Blocking List',
'dns': 'cbl.abuseat.org',
'site': 'http://www.abuseat.org'
},
{
'name': 'Multi SURBL',
'dns': 'multi.surbl.org',
'site': 'http://www.surbl.org'
},
{
'name': 'Passive Spam Block List',
'dns': 'psbl.surriel.com',
'site': 'https://psbl.org'
'name': 'SpamCop',
'dns': 'bl.spamcop.net',
'site': 'http://spamcop.net'
},
{
'name': 'Sorbs Aggregate Zone',
@@ -360,20 +344,30 @@ const RBL_LIST = [
'site': 'http://sorbs.net'
},
{
'name': 'Spam Cannibal',
'dns': 'bl.spamcannibal.org',
'site': 'http://www.spamcannibal.org/cannibal.cgi'
},
{
'name': 'SpamCop',
'dns': 'bl.spamcop.net',
'site': 'http://spamcop.net'
'name': 'Composite Blocking List',
'dns': 'cbl.abuseat.org',
'site': 'http://www.abuseat.org'
},
{
'name': 'SpamHaus Zen',
'dns': 'zen.spamhaus.org',
'site': 'http://spamhaus.org'
},
{
'name': 'Multi SURBL',
'dns': 'multi.surbl.org',
'site': 'http://www.surbl.org'
},
{
'name': 'Spam Cannibal',
'dns': 'bl.spamcannibal.org',
'site': 'http://www.spamcannibal.org/cannibal.cgi'
},
{
'name': 'dnsbl.abuse.ch',
'dns': 'spam.abuse.ch',
'site': 'http://dnsbl.abuse.ch/'
},
{
'name': 'The Unsubscribe Blacklist(UBL)',
'dns': 'ubl.unsubscore.com ',
@@ -397,15 +391,15 @@ function checkRblStatus(domain, callback) {
// https://tools.ietf.org/html/rfc5782
async.map(RBL_LIST, function (rblServer, iteratorDone) {
dns.resolve(flippedIp + '.' + rblServer.dns, 'A', DNS_OPTIONS, function (error, records) {
dig.resolve(flippedIp + '.' + rblServer.dns, 'A', digOptions, function (error, records) {
if (error || !records) return iteratorDone(null, null); // not listed
debug('checkRblStatus: %s (ip: %s) is in the blacklist of %j', domain, flippedIp, rblServer);
var result = _.extend({ }, rblServer);
dns.resolve(flippedIp + '.' + rblServer.dns, 'TXT', DNS_OPTIONS, function (error, txtRecords) {
result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords.map(x => x.join(''));
dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', digOptions, function (error, txtRecords) {
result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords;
debug('checkRblStatus: %s (error: %s) (txtRecords: %j)', domain, error, txtRecords);
@@ -475,59 +469,45 @@ function getStatus(domain, callback) {
function createMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
const mailFqdn = config.mailFqdn();
debug('createMailConfig: generating mail config');
maildb.getAll(function (error, mailDomains) {
maildb.getAll(function (error, mailOutDomains) {
if (error) return callback(error);
user.getOwner(function (error, owner) {
const mailFqdn = config.mailFqdn();
const defaultDomain = config.adminDomain();
const alertsFrom = `no-reply@${defaultDomain}`;
var mailDomain = mailOutDomains[0]; // mail container can only handle one domain at this point
const alertsFrom = `no-reply@${mailDomain.domain}`;
user.getOwner(function (error, owner) {
const alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
alertsTo.concat(error ? [] : owner.email).join(','); // owner may not exist yet
const mailOutDomains = mailDomains.map(function (d) { return d.domain; }).join(',');
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
const mailOutDomain = mailDomain.domain;
const mailInDomain = mailDomain.enabled ? mailDomain.domain : '';
const catchAll = mailDomain.catchAll.map(function (c) { return `${c}@${mailDomain.domain}`; }).join(',');
const mailFromValidation = mailDomain.mailFromValidation;
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_default_domain=${defaultDomain}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\n\n`, 'utf8')) {
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini',
`mail_in_domains=${mailInDomain}\nmail_out_domains=${mailOutDomain}\nmail_default_domain=${mailDomain.domain}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
// enable_outbound makes plugin forward email for relayed mail. non-relayed mail always hits LMTP plugin first
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/smtp_forward.ini'), 'enable_outbound=false\ndomain_selector=mail_from\n', 'utf8')) {
return callback(new Error('Could not create smtp forward file:' + safe.error.message));
var relay = mailDomain.relay;
const enabled = relay.provider !== 'cloudron-smtp' ? true : false,
host = relay.host || '',
port = relay.port || 25,
username = relay.username || '',
password = relay.password || '';
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`enable_outbound=${enabled}\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
// create sections for per-domain configuration
mailDomains.forEach(function (domain) {
const catchAll = domain.catchAll.map(function (c) { return `${c}@${domain.domain}`; }).join(',');
const mailFromValidation = domain.mailFromValidation;
if (!safe.fs.appendFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`[${domain.domain}]\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
const relay = domain.relay;
const enableRelay = relay.provider !== 'cloudron-smtp',
host = relay.host || '',
port = relay.port || 25,
username = relay.username || '',
password = relay.password || '';
if (!enableRelay) return;
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
});
callback(null, mailInDomains.length !== 0 /* allowInbound */);
callback(null, mailInDomain.length !== 0);
});
});
}
@@ -544,7 +524,7 @@ function restartMail(callback) {
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
// admin and mail share the same certificate
reverseProxy.getCertificate({ fqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
reverseProxy.getCertificate({ intrinsicFqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
if (error) return callback(error);
// the setup script copies dhparams.pem to /addons/mail
@@ -602,57 +582,16 @@ function getAll(callback) {
});
}
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
function txtRecordsWithSpf(domain, callback) {
function ensureDkimKey(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
if (error) return callback(error);
var dkimPath = path.join(paths.MAIL_DATA_DIR, `dkim/${domain}`);
var dkimPrivateKeyFile = path.join(dkimPath, 'private');
var dkimPublicKeyFile = path.join(dkimPath, 'public');
var dkimSelectorFile = path.join(dkimPath, 'selector');
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
var i, matches, validSpf;
for (i = 0; i < txtRecords.length; i++) {
matches = txtRecords[i].match(/^("?v=spf1) /); // DO backend may return without quotes
if (matches === null) continue;
// this won't work if the entry is arbitrarily "split" across quoted strings
validSpf = txtRecords[i].indexOf('a:' + config.mailFqdn()) !== -1;
break; // there can only be one SPF record
}
if (validSpf) return callback(null, null);
if (!matches) { // no spf record was found, create one
txtRecords.push('"v=spf1 a:' + config.mailFqdn() + ' ~all"');
debug('txtRecordsWithSpf: adding txt record');
} else { // just add ourself
txtRecords[i] = matches[1] + ' a:' + config.mailFqdn() + txtRecords[i].slice(matches[1].length);
debug('txtRecordsWithSpf: inserting txt record');
}
return callback(null, txtRecords);
});
}
function ensureDkimKeySync(domain) {
assert.strictEqual(typeof domain, 'string');
const dkimPath = path.join(paths.MAIL_DATA_DIR, `dkim/${domain}`);
const dkimPrivateKeyFile = path.join(dkimPath, 'private');
const dkimPublicKeyFile = path.join(dkimPath, 'public');
const dkimSelectorFile = path.join(dkimPath, 'selector');
if (safe.fs.existsSync(dkimPublicKeyFile) &&
safe.fs.existsSync(dkimPublicKeyFile) &&
safe.fs.existsSync(dkimPublicKeyFile)) {
debug(`Reusing existing DKIM keys for ${domain}`);
return null;
}
debug(`Generating new DKIM keys for ${domain}`);
debug('Generating new DKIM keys');
if (!safe.fs.mkdirSync(dkimPath) && safe.error.code !== 'EEXIST') {
debug('Error creating dkim.', safe.error);
@@ -664,7 +603,41 @@ function ensureDkimKeySync(domain) {
if (!safe.fs.writeFileSync(dkimSelectorFile, config.dkimSelector(), 'utf8')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
return null;
callback();
}
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
function txtRecordsWithSpf(callback) {
assert.strictEqual(typeof callback, 'function');
domains.getDNSRecords('', config.adminDomain(), 'TXT', function (error, txtRecords) {
if (error) return callback(error);
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
var i, matches, validSpf;
for (i = 0; i < txtRecords.length; i++) {
matches = txtRecords[i].match(/^("?v=spf1) /); // DO backend may return without quotes
if (matches === null) continue;
// this won't work if the entry is arbitrarily "split" across quoted strings
validSpf = txtRecords[i].indexOf('a:' + config.adminFqdn()) !== -1;
break; // there can only be one SPF record
}
if (validSpf) return callback(null, null);
if (!matches) { // no spf record was found, create one
txtRecords.push('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
debug('txtRecordsWithSpf: adding txt record');
} else { // just add ourself
txtRecords[i] = matches[1] + ' a:' + config.adminFqdn() + txtRecords[i].slice(matches[1].length);
debug('txtRecordsWithSpf: inserting txt record');
}
return callback(null, txtRecords);
});
}
function readDkimPublicKeySync(domain) {
@@ -690,9 +663,6 @@ function addDnsRecords(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var error = ensureDkimKeySync(domain);
if (error) return callback(error);
if (process.env.BOX_ENV === 'test') return callback();
var dkimKey = readDkimPublicKeySync(domain);
@@ -706,21 +676,28 @@ function addDnsRecords(domain, callback) {
debug('addDnsRecords: %j', records);
txtRecordsWithSpf(domain, function (error, txtRecords) {
if (error) return callback(error);
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords });
if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords });
debug('addDnsRecords: will update %j', records);
debug('addDnsRecords: will update %j', records);
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDNSRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
callback(error);
retryCallback(error);
});
});
}, function (error) {
if (error) debug('addDnsRecords: done updating records with error:', error);
else debug('addDnsRecords: done');
callback(error);
});
}
@@ -728,28 +705,18 @@ function add(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
maildb.add(domain, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'Domain already exists'));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'No such domain'));
ensureDkimKey(domain, function (error) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
addDnsRecords(domain, NOOP_CALLBACK); // add the required dns records asynchronously
maildb.add(domain, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'Domain already exists'));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'No such domain'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback();
});
}
addDnsRecords(domain, NOOP_CALLBACK); // add the required dns records asynchronously
// this is just a way to resync the mail "dns" records via the UI
function update(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error) {
if (error) return callback(error);
addDnsRecords(domain, NOOP_CALLBACK);
callback();
callback();
});
});
}
@@ -757,18 +724,11 @@ function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, result) {
if (error) return callback(error);
maildb.del(domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, error.message));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
maildb.del(domain, function (error) {
if (error && error.reason === DatabaseError.IN_USE) return callback(new MailError(MailError.IN_USE));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, error.message));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (result && result.enabled) restartMail(NOOP_CALLBACK);
callback();
});
callback();
});
}
@@ -840,7 +800,7 @@ function setMailEnabled(domain, enabled, callback) {
];
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDnsRecords(record.subdomain, domain, record.type, record.values, iteratorCallback);
domains.upsertDNSRecords(record.subdomain, domain, record.type, record.values, iteratorCallback);
}, NOOP_CALLBACK);
callback(null);
@@ -872,17 +832,6 @@ function getMailboxes(domain, callback) {
});
}
function removeMailboxes(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.delByDomain(domain, function (error) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback();
});
}
function getUserMailbox(domain, userId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
@@ -891,7 +840,6 @@ function getUserMailbox(domain, userId, callback) {
user.get(userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such user'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (!result.username) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
mailboxdb.getMailbox(result.username, domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
@@ -910,7 +858,6 @@ function enableUserMailbox(domain, userId, callback) {
user.get(userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such user'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR));
if (!result.username) return callback(new MailError(MailError.NOT_FOUND, 'user has no username'));
mailboxdb.add(result.username, domain, userId, mailboxdb.TYPE_USER, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'mailbox already exists'));
@@ -929,7 +876,6 @@ function disableUserMailbox(domain, userId, callback) {
user.get(userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such user'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (!result.username) return callback(new MailError(MailError.NOT_FOUND, 'user has no username'));
mailboxdb.del(result.username, domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
@@ -948,7 +894,8 @@ function getAliases(domain, userId, callback) {
user.get(userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such user'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (!result.username) return callback(new MailError(MailError.NOT_FOUND, 'user has no username'));
if (!result.username) return callback(null, []);
mailboxdb.getAliasesForName(result.username, domain, function (error, aliases) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
@@ -975,7 +922,6 @@ function setAliases(domain, userId, aliases, callback) {
user.get(userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such user'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (!result.username) return callback(new MailError(MailError.NOT_FOUND, 'user has no username'));
mailboxdb.setAliasesForName(result.username, domain, aliases, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, error.message));
+1 -1
View File
@@ -9,7 +9,7 @@ This is most likely a problem in the application.
To resolve this, you can try the following:
* Restart the app in the app configuration dialog
* Restore the app to the latest backup
* Contact us via support@cloudron.io or https://forum.cloudron.io
* Contact us via support@cloudron.io or https://chat.cloudron.io
Powered by https://cloudron.io
-13
View File
@@ -17,7 +17,6 @@ exports = module.exports = {
getByOwnerId: getByOwnerId,
delByOwnerId: delByOwnerId,
delByDomain: delByDomain,
updateName: updateName,
@@ -73,18 +72,6 @@ function del(name, domain, callback) {
});
}
function delByDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mailboxes WHERE domain = ?', [ domain ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function delByOwnerId(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
-1
View File
@@ -61,7 +61,6 @@ function del(domain, callback) {
// deletes aliases as well
database.query('DELETE FROM mail WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
-35
View File
@@ -1,35 +0,0 @@
'use strict';
exports = module.exports = {
resolve: resolve
};
var assert = require('assert'),
dns = require('dns');
// a note on TXT records. It doesn't have quotes ("") at the DNS level. Those quotes
// are added for DNS server software to enclose spaces. Such quotes may also be returned
// by the DNS REST API of some providers
function resolve(hostname, rrtype, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof rrtype, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
const resolver = new dns.Resolver();
if (options.server) resolver.setServers([ options.server ]);
// should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814
const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000);
resolver.resolve(hostname, rrtype, function (error, result) {
clearTimeout(timerId);
if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT';
// result is an empty array if there was no error but there is no record. when you query a random
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
// type (CNAME) it is not an error and empty array
callback(error, result);
});
}
+1 -1
View File
@@ -33,7 +33,7 @@
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
</form>
<a href="/api/v1/session/password/resetRequest.html">Reset password</a>
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
</div>
</div>
</div>
+1 -1
View File
@@ -5,7 +5,7 @@
<div class="layout-content">
<center>
<h2>Reset password</h2>
<h2>Reset your password</h2>
</center>
<br/>
+38 -28
View File
@@ -32,7 +32,6 @@ var acme = require('./cert/acme.js'),
caas = require('./cert/caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:certificates'),
domains = require('./domains.js'),
ejs = require('ejs'),
@@ -40,12 +39,12 @@ var acme = require('./cert/acme.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
tld = require('tldjs'),
user = require('./user.js'),
util = require('util');
@@ -85,11 +84,12 @@ function getApi(app, callback) {
if (domain.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
var api = domain.tlsConfig.provider === 'caas' ? caas : acme;
// use acme if we have altDomain or the tlsConfig is not caas
var api = (app.altDomain || domain.tlsConfig.provider !== 'caas') ? acme : caas;
var options = { };
if (domain.tlsConfig.provider === 'caas') {
options.prod = true;
options.prod = true; // with altDomain, we will choose acme setting based on this
} else { // acme
options.prod = domain.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
}
@@ -138,11 +138,22 @@ function validateCertificate(domain, cert, key) {
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${domain}"`, { encoding: 'utf8', input: cert });
var result = safe.child_process.execSync('openssl x509 -noout -checkhost "' + domain + '"', { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${domain}`);
// if no match, check alt names
if (result.indexOf('does match certificate') === -1) {
// https://github.com/drwetter/testssl.sh/pull/383
var cmd = 'openssl x509 -noout -text | grep -A3 "Subject Alternative Name" | \
grep "DNS:" | \
sed -e "s/DNS://g" -e "s/ //g" -e "s/,/ /g" -e "s/othername:<unsupported>//g"';
result = safe.child_process.execSync(cmd, { encoding: 'utf8', input: cert });
var altNames = result ? [ ] : result.trim().split(' '); // might fail if cert has no SAN
debug('validateCertificate: detected altNames as %j', altNames);
// check altNames
if (!altNames.some(matchesDomain)) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, util.format('Certificate is not valid for this domain. Expecting %s in %j', domain, altNames));
}
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
@@ -175,14 +186,8 @@ function setFallbackCertificate(domain, fallback, callback) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
} else if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${domain}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`);
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=*.%s -nodes', keyFilePath, certFilePath, domain);
if (!safe.child_process.execSync(certCommand)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
safe.fs.unlinkSync(configFile);
}
platform.handleCertChanged('*.' + domain);
@@ -215,13 +220,15 @@ function getCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.key`);
var vhost = app.altDomain || app.intrinsicFqdn;
var certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.key`);
certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
@@ -233,7 +240,7 @@ function ensureCertificate(app, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const vhost = app.fqdn;
var vhost = app.altDomain || app.intrinsicFqdn;
var certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
@@ -271,7 +278,7 @@ function ensureCertificate(app, auditSource, callback) {
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: vhost, errorMessage: errorMessage });
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
if (!certFilePath || !keyFilePath) return getFallbackCertificate(app.domain, callback);
if (!certFilePath || !keyFilePath) return getFallbackCertificate(app.altDomain ? tld.getDomain(app.altDomain) : app.domain, callback);
callback(null, { certFilePath, keyFilePath, reason: 'new-le' });
});
@@ -307,7 +314,7 @@ function configureAdmin(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var adminApp = { domain: config.adminDomain(), fqdn: config.adminFqdn() };
var adminApp = { domain: config.adminDomain(), intrinsicFqdn: config.adminFqdn() };
ensureCertificate(adminApp, auditSource, function (error, bundle) {
if (error) return callback(error);
@@ -322,11 +329,12 @@ function configureAppInternal(app, bundle, callback) {
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
var vhost = app.altDomain || app.intrinsicFqdn;
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: app.fqdn,
vhost: vhost,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
@@ -338,10 +346,10 @@ function configureAppInternal(app, bundle, callback) {
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debug('writing config for "%s" to %s with options %j', app.fqdn, nginxConfigFilename, data);
debug('writing config for "%s" to %s with options %j', vhost, nginxConfigFilename, data);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx config for "%s" : %s', app.fqdn, safe.error.message);
debug('Error creating nginx config for "%s" : %s', vhost, safe.error.message);
return callback(safe.error);
}
@@ -364,9 +372,11 @@ function unconfigureApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || app.intrinsicFqdn;
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', app.fqdn, safe.error.message);
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', vhost, safe.error.message);
return callback(null);
}
@@ -382,21 +392,21 @@ function renewAll(auditSource, callback) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ domain: config.adminDomain(), fqdn: config.adminFqdn() }); // inject fake webadmin app
allApps.push({ domain: config.adminDomain(), intrinsicFqdn: config.adminFqdn() }); // inject fake webadmin app
async.eachSeries(allApps, function (app, iteratorCallback) {
ensureCertificate(app, auditSource, function (error, bundle) {
if (bundle.reason !== 'new-le' && bundle.reason !== 'fallback') return iteratorCallback();
// reconfigure for the case where we got a renewed cert after fallback
var configureFunc = app.fqdn === config.adminFqdn() ?
var configureFunc = app.intrinsicFqdn === config.adminFqdn() ?
configureAdminInternal.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: configureAppInternal.bind(null, app, bundle);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
platform.handleCertChanged(app.fqdn);
platform.handleCertChanged(app.intrinsicFqdn);
iteratorCallback(); // move to next app
});
+6 -4
View File
@@ -58,6 +58,8 @@ function removeInternalAppFields(app) {
iconUrl: app.iconUrl,
fqdn: app.fqdn,
memoryLimit: app.memoryLimit,
altDomain: app.altDomain,
cnameTarget: app.cnameTarget,
xFrameOptions: app.xFrameOptions,
sso: app.sso,
debugMode: app.debugMode,
@@ -132,6 +134,9 @@ function installApp(req, res, next) {
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
// falsy value in altDomain unsets it
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
@@ -176,6 +181,7 @@ function configureApp(req, res, next) {
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
@@ -237,13 +243,9 @@ function cloneApp(req, res, next) {
apps.clone(req.params.id, data, auditSource(req), function (error, result) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
-1
View File
@@ -89,7 +89,6 @@ function feedback(req, res, next) {
if (VALID_TYPES.indexOf(req.body.type) === -1) return next(new HttpError(400, 'unknown type'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
appstore.sendFeedback(_.extend(req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return next(new HttpError(402, 'Login to App Store to create support tickets. You can also email support@cloudron.io'));
+6 -3
View File
@@ -9,14 +9,17 @@ var developer = require('../developer.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function login(req, res, next) {
passport.authenticate('local', function (error, user) {
if (error) return next(new HttpError(500, error));
if (!user) return next(new HttpError(401, 'Invalid credentials'));
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
developer.issueDeveloperToken(user, ip, function (error, result) {
developer.issueDeveloperToken(user, auditSource(req), function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { token: result.token, expiresAt: result.expiresAt }));
+1 -7
View File
@@ -29,9 +29,6 @@ function add(req, res, next) {
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
domains.add(req.body.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === DomainError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === DomainError.BAD_FIELD) return next(new HttpError(400, error.message));
@@ -75,9 +72,6 @@ function update(req, res, next) {
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
domains.update(req.params.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainError.BAD_FIELD) return next(new HttpError(400, error.message));
@@ -93,7 +87,7 @@ function del(req, res, next) {
domains.del(req.params.domain, function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainError.IN_USE) return next(new HttpError(409, 'Domain is still in use. Remove all apps using this domain'));
if (error && error.reason === DomainError.IN_USE) return next(new HttpError(409, 'Domain is still in use'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
+1 -5
View File
@@ -15,14 +15,10 @@ function get(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
if (req.query.actions && typeof req.query.actions !== 'string') return next(new HttpError(400, 'actions must be a comma separated string'));
if (req.query.action && typeof req.query.action !== 'string') return next(new HttpError(400, 'action must be a string'));
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
var actions = req.query.actions ? req.query.actions.split(',').map(function (s) { return s.trim(); }) : [];
if (req.query.action) actions.push(req.query.action);
eventlog.getAllPaged(actions, req.query.search || null, page, perPage, function (error, result) {
eventlog.getAllPaged(req.query.action || null, req.query.search || null, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { eventlogs: result }));
-17
View File
@@ -4,7 +4,6 @@ exports = module.exports = {
get: get,
add: add,
update: update,
del: del,
getStatus: getStatus,
@@ -61,24 +60,11 @@ function add(req, res, next) {
});
}
function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.domain, 'string');
mail.update(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function del(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.del(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.IN_USE) return next(new HttpError(409, 'Mail domain is still in use. Remove existing mailboxes'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
@@ -88,9 +74,6 @@ function del(req, res, next) {
function getStatus(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
// can take a while to query all the DNS entries
req.clearTimeout();
mail.getStatus(req.params.domain, function (error, records) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
+5 -6
View File
@@ -26,10 +26,9 @@ var apps = require('../apps'),
util = require('util'),
_ = require('underscore');
// appObject is optional here
function auditSource(req, appId, appObject) {
function auditSource(req, appId) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { authType: 'oauth', ip: ip, appId: appId, app: appObject };
return { authType: 'oauth', ip: ip, appId: appId };
}
// create OAuth 2.0 server
@@ -240,7 +239,7 @@ function loginForm(req, res) {
apps.get(result.appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
var applicationName = result.fqdn;
var applicationName = result.altDomain || result.intrinsicFqdn;
render(applicationName, '/api/v1/apps/' + result.id + '/icon');
});
});
@@ -448,7 +447,7 @@ var authorization = [
var type = req.oauth2.client.type;
if (type === clients.TYPE_EXTERNAL || type === clients.TYPE_BUILT_IN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id, user: user.removePrivateFields(req.oauth2.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id });
return next();
}
@@ -459,7 +458,7 @@ var authorization = [
if (error) return sendError(req, res, 'Internal error');
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id, appObject), { userId: req.oauth2.user.id, user: user.removePrivateFields(req.oauth2.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id), { userId: req.oauth2.user.id });
next();
});
+6 -30
View File
@@ -1,11 +1,8 @@
'use strict';
exports = module.exports = {
getAppAutoupdatePattern: getAppAutoupdatePattern,
setAppAutoupdatePattern: setAppAutoupdatePattern,
getBoxAutoupdatePattern: getBoxAutoupdatePattern,
setBoxAutoupdatePattern: setBoxAutoupdatePattern,
getAutoupdatePattern: getAutoupdatePattern,
setAutoupdatePattern: setAutoupdatePattern,
getCloudronName: getCloudronName,
setCloudronName: setCloudronName,
@@ -30,41 +27,20 @@ var assert = require('assert'),
settings = require('../settings.js'),
SettingsError = settings.SettingsError;
function getAppAutoupdatePattern(req, res, next) {
settings.getAppAutoupdatePattern(function (error, pattern) {
function getAutoupdatePattern(req, res, next) {
settings.getAutoupdatePattern(function (error, pattern) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { pattern: pattern }));
});
}
function setAppAutoupdatePattern(req, res, next) {
function setAutoupdatePattern(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setAppAutoupdatePattern(req.body.pattern, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function getBoxAutoupdatePattern(req, res, next) {
settings.getBoxAutoupdatePattern(function (error, pattern) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { pattern: pattern }));
});
}
function setBoxAutoupdatePattern(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setBoxAutoupdatePattern(req.body.pattern, function (error) {
settings.setAutoupdatePattern(req.body.pattern, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
+76 -47
View File
@@ -9,23 +9,25 @@
var appdb = require('../../appdb.js'),
apps = require('../../apps.js'),
assert = require('assert'),
path = require('path'),
async = require('async'),
child_process = require('child_process'),
clients = require('../../clients.js'),
config = require('../../config.js'),
constants = require('../../constants.js'),
apphealthmonitor = require('../../apphealthmonitor.js'),
database = require('../../database.js'),
docker = require('../../docker.js').connection,
domains = require('../../domains.js'),
expect = require('expect.js'),
fs = require('fs'),
hock = require('hock'),
http = require('http'),
https = require('https'),
js2xml = require('js2xmlparser').parse,
ldap = require('../../ldap.js'),
mail = require('../../mail.js'),
net = require('net'),
nock = require('nock'),
path = require('path'),
paths = require('../../paths.js'),
safe = require('safetydance'),
server = require('../../server.js'),
@@ -41,8 +43,9 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '25.4.0';
var TEST_IMAGE_TAG = '25.2.0';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
const DOMAIN_0 = {
domain: 'example-apps-test.com',
@@ -57,6 +60,7 @@ const CLOUDRON_ID = 'somecloudronid';
var APP_STORE_ID = 'test', APP_ID;
var APP_LOCATION = 'appslocation';
var APP_DOMAIN = 'example-apps-test.com';
var APP_LOCATION_2 = 'appslocationtwo';
var APP_LOCATION_NEW = 'appslocationnew';
@@ -66,19 +70,14 @@ APP_MANIFEST.dockerImage = TEST_IMAGE;
var APP_MANIFEST_1 = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
APP_MANIFEST_1.dockerImage = TEST_IMAGE;
const USERNAME = 'superadmin';
const PASSWORD = 'Foobar?1337';
const EMAIL ='admin@me.com';
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='admin@me.com';
var USER_1_ID = null, USERNAME_1 = 'user', EMAIL_1 ='user@me.com';
const USER_1_APPSTORE_TOKEN = 'appstoretoken';
const USERNAME_1 = 'user';
const EMAIL_1 ='user@me.com';
var user_1_id = null;
// authentication token
var token = null;
var token = null; // authentication token
var token_1 = null;
var awsHostedZones;
function startDockerProxy(interceptor, callback) {
assert.strictEqual(typeof interceptor, 'function');
@@ -120,7 +119,6 @@ function checkAddons(appEntry, done) {
delete body.recvmail; // unclear why dovecot mail delivery won't work
delete body.stdenv; // cannot access APP_ORIGIN
delete body.email; // sieve will fail not sure why yet
for (var key in body) {
if (body[key] !== 'OK') return callback('Not done yet: ' + JSON.stringify(body));
@@ -166,6 +164,21 @@ function startBox(done) {
safe.fs.unlinkSync(paths.INFRA_VERSION_FILE);
child_process.execSync('docker ps -qa | xargs --no-run-if-empty docker rm -f');
// awsHostedZones = {
// HostedZones: [{
// Id: '/hostedzone/ZONEID',
// Name: config.zoneName() + '.',
// CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30',
// ResourceRecordSetCount: 2,
// ChangeInfo: {
// Id: '/change/CKRTFJA0ANHXB',
// Status: 'INSYNC'
// }
// }],
// IsTruncated: false,
// MaxItems: '100'
// };
async.series([
// first clear, then start server. otherwise, taskmanager spins up tasks for obsolete appIds
database.initialize,
@@ -205,7 +218,7 @@ function startBox(done) {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
user_1_id = res.body.id;
USER_1_ID = res.body.id;
callback(null);
});
@@ -215,7 +228,7 @@ function startBox(done) {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, user_1_id, 'test-client-id', Date.now() + 1000000, '*', callback);
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
},
function (callback) {
@@ -250,7 +263,6 @@ function stopBox(done) {
// db is not cleaned up here since it's too late to call it after server.stop. if called before server.stop taskmanager apptasks are unhappy :/
async.series([
apphealthmonitor.stop,
taskmanager.stopPendingTasks,
taskmanager.waitForPendingTasks,
appdb._clear,
@@ -444,11 +456,11 @@ describe('App API', function () {
});
it('app install succeeds with purchase', function (done) {
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons') >= 0; }, { 'domain': DOMAIN_0.domain }).reply(201, { cloudron: { id: CLOUDRON_ID } });
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + USER_1_ID + '/cloudrons') >= 0; }, { 'domain': DOMAIN_0.domain }).reply(201, { cloudron: { id: CLOUDRON_ID } });
var fake2 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + USER_1_ID + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
settings.setAppstoreConfig({ userId: user_1_id, token: USER_1_APPSTORE_TOKEN }, function (error) {
settings.setAppstoreConfig({ userId: USER_1_ID, token: USER_1_APPSTORE_TOKEN }, function (error) {
if (error) return done(error);
expect(fake1.isDone()).to.be.ok();
@@ -560,8 +572,8 @@ describe('App API', function () {
});
it('can uninstall app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(200, { });
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(204, { });
var fake1 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/' + USER_1_ID + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(200, { });
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/' + USER_1_ID + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(204, { });
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
@@ -576,7 +588,7 @@ describe('App API', function () {
it('app install succeeds again', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + USER_1_ID + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
@@ -625,15 +637,17 @@ describe('App API', function () {
});
});
describe('App installation', function () {
xdescribe('App installation', function () {
this.timeout(100000);
var apiHockInstance = hock.createHock({ throwOnUnmatched: false }), apiHockServer;
var awsHockInstance = hock.createHock({ throwOnUnmatched: false }), awsHockServer;
// *.foobar.com
var validCert1, validKey1;
before(function (done) {
child_process.execSync('openssl req -subj "/CN=*.' + DOMAIN_0.domain + '/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt');
child_process.execSync('openssl req -subj "/CN=*.foobar.com/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt');
validKey1 = fs.readFileSync('/tmp/server.key', 'utf8');
validCert1 = fs.readFileSync('/tmp/server.crt', 'utf8');
@@ -641,37 +655,53 @@ describe('App installation', function () {
async.series([
startBox,
apphealthmonitor.start,
function (callback) {
apiHockInstance
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
.replyWithFile(200, path.resolve(__dirname, '../../../assets/avatar.png'));
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'));
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.adminDomain(), config.zoneName()),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
function (callback) {
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons') >= 0; }, { 'domain': DOMAIN_0.domain }).reply(201, { cloudron: { id: CLOUDRON_ID } });
settings.setAppstoreConfig({ userId: user_1_id, token: USER_1_APPSTORE_TOKEN }, function (error) {
if (error) return callback(error);
awsHockInstance
.get('/2013-04-01/hostedzone')
.max(Infinity)
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }), { 'Content-Type': 'application/xml' })
.filteringPathRegEx(/name=[^&]*/, 'name=location')
.get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=location&type=A')
.max(Infinity)
.reply(200, js2xml('ListResourceRecordSetsResponse', { ResourceRecordSets: [ ] }, { 'Content-Type': 'application/xml' }))
.filteringRequestBody(function (unusedBody) { return ''; }) // strip out body
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
.max(Infinity)
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'dnsrecordid', Status: 'INSYNC' } }), { 'Content-Type': 'application/xml' });
expect(fake1.isDone()).to.be.ok();
callback();
});
awsHockServer = http.createServer(awsHockInstance.handler).listen(5353, callback);
}
], done);
});
after(stopBox);
after(function (done) {
APP_ID = null;
async.series([
apiHockServer.close.bind(apiHockServer),
awsHockServer.close.bind(awsHockServer),
stopBox
], done);
});
var appResult = null, appEntry = null;
it('can install test app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var count = 0;
function checkInstallStatus() {
@@ -688,11 +718,10 @@ describe('App installation', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
APP_ID = res.body.id;
checkInstallStatus();
});
@@ -721,8 +750,8 @@ describe('App installation', function () {
expect(data.Config.Env).to.contain('WEBADMIN_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('API_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('CLOUDRON=1');
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + APP_LOCATION + '.' + DOMAIN_0.domain);
expect(data.Config.Env).to.contain('APP_DOMAIN=' + APP_LOCATION + '.' + DOMAIN_0.domain);
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + APP_LOCATION + '.' + APP_DOMAIN);
expect(data.Config.Env).to.contain('APP_DOMAIN=' + APP_LOCATION + '.' + APP_DOMAIN);
// Hostname must not be set of app fqdn or app location!
expect(data.Config.Hostname).to.not.contain(APP_LOCATION);
expect(data.Config.Env).to.contain('ECHO_SERVER_PORT=7171');
@@ -1100,8 +1129,8 @@ describe('App installation', function () {
});
it('can uninstall app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(200, { });
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(204, { });
var fake2 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(200, { });
var fake3 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(204, { });
var count = 0;
function checkUninstallStatus() {
@@ -1119,10 +1148,6 @@ describe('App installation', function () {
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
checkUninstallStatus();
});
});
@@ -1148,7 +1173,11 @@ describe('App installation', function () {
it('uninstalled - unregistered subdomain', function (done) {
apiHockInstance.done(function (error) { // checks if all the apiHockServer APIs were called
expect(!error).to.be.ok();
done();
awsHockInstance.done(function (error) {
expect(!error).to.be.ok();
done();
});
});
});
+1 -129
View File
@@ -6,16 +6,11 @@
/* global after:false */
var async = require('async'),
child_process = require('child_process'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
fs = require('fs'),
path = require('path'),
paths = require('../../paths.js'),
superagent = require('superagent'),
server = require('../../server.js'),
_ = require('underscore');
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -133,17 +128,6 @@ describe('Domains API', function () {
});
});
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ })
.send(DOMAIN_0)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
@@ -268,116 +252,4 @@ describe('Domains API', function () {
});
});
});
describe('Certificates API', function () {
var validCert0, validKey0, // example.com
validCert1, validKey1; // *.example.com
before(function (done) {
child_process.execSync(`openssl req -subj "/CN=${DOMAIN_0.domain}/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt`);
validKey0 = fs.readFileSync('/tmp/server.key', 'utf8');
validCert0 = fs.readFileSync('/tmp/server.crt', 'utf8');
child_process.execSync(`openssl req -subj "/CN=*.${DOMAIN_0.domain}/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt`);
validKey1 = fs.readFileSync('/tmp/server.key', 'utf8');
validCert1 = fs.readFileSync('/tmp/server.crt', 'utf8');
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send(DOMAIN_0)
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
done();
});
});
it('cannot set certificate without certificate', function (done) {
var d = _.extend({}, DOMAIN_0);
d.fallbackCertificate = { key: validKey1 };
superagent.put(`${SERVER_URL}/api/v1/domains/${DOMAIN_0.domain}`)
.query({ access_token: token })
.send(d)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate without key', function (done) {
var d = _.extend({}, DOMAIN_0);
d.fallbackCertificate = { cert: validCert1 };
superagent.put(`${SERVER_URL}/api/v1/domains/${DOMAIN_0.domain}`)
.query({ access_token: token })
.send(d)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate with cert not being a string', function (done) {
var d = _.extend({}, DOMAIN_0);
d.fallbackCertificate = { cert: 1234, key: validKey1 };
superagent.put(`${SERVER_URL}/api/v1/domains/${DOMAIN_0.domain}`)
.query({ access_token: token })
.send(d)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate with key not being a string', function (done) {
var d = _.extend({}, DOMAIN_0);
d.fallbackCertificate = { cert: validCert1, key: true };
superagent.put(`${SERVER_URL}/api/v1/domains/${DOMAIN_0.domain}`)
.query({ access_token: token })
.send(d)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set non-fallback certificate', function (done) {
var d = _.extend({}, DOMAIN_0);
d.fallbackCertificate = { cert: validCert0, key: validKey0 };
superagent.put(`${SERVER_URL}/api/v1/domains/${DOMAIN_0.domain}`)
.query({ access_token: token })
.send(d)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('can set fallback certificate', function (done) {
var d = _.extend({}, DOMAIN_0);
d.fallbackCertificate = { cert: validCert1, key: validKey1 };
superagent.put(`${SERVER_URL}/api/v1/domains/${DOMAIN_0.domain}`)
.query({ access_token: token })
.send(d)
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('did set the certificate', function (done) {
var cert = fs.readFileSync(path.join(paths.APP_CERTS_DIR, `${DOMAIN_0.domain}.host.cert`), 'utf-8');
expect(cert).to.eql(validCert1);
var key = fs.readFileSync(path.join(paths.APP_CERTS_DIR, `${DOMAIN_0.domain}.host.key`), 'utf-8');
expect(key).to.eql(validKey1);
done();
});
});
});
+2 -15
View File
@@ -77,8 +77,6 @@ function cleanup(done) {
}
describe('Eventlog API', function () {
this.timeout(10000);
before(setup);
after(cleanup);
@@ -113,7 +111,7 @@ describe('Eventlog API', function () {
});
});
it('succeeds with deprecated action', function (done) {
it('succeeds with action', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token, page: 1, per_page: 10, action: 'cloudron.activate' })
.end(function (error, result) {
@@ -124,17 +122,6 @@ describe('Eventlog API', function () {
});
});
it('succeeds with actions', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token, page: 1, per_page: 10, actions: 'cloudron.activate, user.add' })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(3);
done();
});
});
it('succeeds with search', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL })
@@ -148,7 +135,7 @@ describe('Eventlog API', function () {
it('succeeds with search', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL, actions: 'cloudron.activate' })
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL, action: 'cloudron.activate' })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(0);
+57 -67
View File
@@ -198,18 +198,16 @@ describe('Mail API', function () {
this.timeout(10000);
before(function (done) {
var dns = require('../../native-dns.js');
var dig = require('../../dig.js');
// replace dns resolveTxt()
resolve = dns.resolve;
dns.resolve = function (hostname, type, options, callback) {
resolve = dig.resolve;
dig.resolve = function (hostname, type, options, callback) {
expect(hostname).to.be.a('string');
expect(callback).to.be.a('function');
if (!dnsAnswerQueue[hostname] || !(type in dnsAnswerQueue[hostname])) return callback(new Error('no mock answer'));
if (dnsAnswerQueue[hostname][type] === null) return callback(new Error({ code: 'ENODATA'} ));
callback(null, dnsAnswerQueue[hostname][type]);
};
@@ -224,13 +222,13 @@ describe('Mail API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
after(function (done) {
var dns = require('../../native-dns.js');
var dig = require('../../dig.js');
dns.resolve = resolve;
dig.resolve = resolve;
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
@@ -270,20 +268,20 @@ describe('Mail API', function () {
expect(res.body.dns.dkim.domain).to.eql(dkimDomain);
expect(res.body.dns.dkim.type).to.eql('TXT');
expect(res.body.dns.dkim.value).to.eql(null);
expect(res.body.dns.dkim.expected).to.eql('v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
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.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql(null);
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.type).to.eql('TXT');
expect(res.body.dns.dmarc.value).to.eql(null);
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.expected).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.dmarc.status).to.eql(false);
expect(res.body.dns.mx).to.be.an('object');
@@ -295,7 +293,7 @@ describe('Mail API', function () {
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.type).to.eql('PTR');
// expect(res.body.ptr.value).to.eql(null); this will be anything random
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn() + '.');
expect(res.body.dns.ptr.status).to.eql(false);
done();
@@ -316,17 +314,17 @@ describe('Mail API', function () {
expect(res.statusCode).to.equal(200);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.spf.value).to.eql(null);
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.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.value).to.eql(null);
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.expected).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.dmarc.status).to.eql(false);
expect(res.body.dns.dmarc.value).to.eql(null);
@@ -336,7 +334,7 @@ describe('Mail API', function () {
expect(res.body.dns.mx.value).to.eql(null);
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn() + '.');
expect(res.body.dns.ptr.status).to.eql(false);
// expect(res.body.ptr.value).to.eql(null); this will be anything random
@@ -347,10 +345,10 @@ describe('Mail API', function () {
it('succeeds with all different spf, dkim, dmarc, mx, ptr records', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: config.mailFqdn() }, { priority: '30', exchange: config.mailFqdn() } ];
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC2; p=reject; pct=100']];
dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain)]];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:random.com ~all']];
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: config.mailFqdn() + '.' }, { priority: '30', exchange: config.mailFqdn() + '.'} ];
dnsAnswerQueue[dmarcDomain].TXT = ['"v=DMARC2; p=reject; pct=100"'];
dnsAnswerQueue[dkimDomain].TXT = ['"v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain) + '"'];
dnsAnswerQueue[spfDomain].TXT = ['"v=spf1 a:random.com ~all"'];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
@@ -358,19 +356,19 @@ describe('Mail API', function () {
expect(res.statusCode).to.equal(200);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' a:random.com ~all');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:' + config.adminFqdn() + ' a:random.com ~all"');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.spf.value).to.eql('v=spf1 a:random.com ~all');
expect(res.body.dns.spf.value).to.eql('"v=spf1 a:random.com ~all"');
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.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.value).to.eql('v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
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');
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.expected).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.dmarc.status).to.eql(false);
expect(res.body.dns.dmarc.value).to.eql('v=DMARC2; p=reject; pct=100');
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);
@@ -378,7 +376,7 @@ describe('Mail API', function () {
expect(res.body.dns.mx.value).to.eql('20 ' + config.mailFqdn() + '. 30 ' + config.mailFqdn() + '.');
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn() + '.');
expect(res.body.dns.ptr.status).to.eql(false);
// expect(res.body.ptr.value).to.eql(null); this will be anything random
@@ -391,7 +389,7 @@ describe('Mail API', function () {
it('succeeds with existing embedded spf', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all']];
dnsAnswerQueue[spfDomain].TXT = ['"v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all"'];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
@@ -401,8 +399,8 @@ describe('Mail API', function () {
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all');
expect(res.body.dns.spf.value).to.eql('"v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all"');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all"');
expect(res.body.dns.spf.status).to.eql(true);
done();
@@ -412,10 +410,10 @@ describe('Mail API', function () {
it('succeeds with all correct records', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[mxDomain].MX = [ { priority: '10', exchange: config.mailFqdn() } ];
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC1; p=reject; pct=100']];
dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM1; t=s; p=', mail._readDkimPublicKeySync(DOMAIN_0.domain) ]];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:' + config.adminFqdn() + ' ~all']];
dnsAnswerQueue[mxDomain].MX = [ { priority: '10', exchange: config.mailFqdn() + '.' } ];
dnsAnswerQueue[dmarcDomain].TXT = ['"v=DMARC1; p=reject; pct=100"'];
dnsAnswerQueue[dkimDomain].TXT = ['"v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain) + '"'];
dnsAnswerQueue[spfDomain].TXT = ['"v=spf1 a:' + config.adminFqdn() + ' ~all"'];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
@@ -425,21 +423,21 @@ describe('Mail API', function () {
expect(res.body.dns.dkim).to.be.an('object');
expect(res.body.dns.dkim.domain).to.eql(dkimDomain);
expect(res.body.dns.dkim.type).to.eql('TXT');
expect(res.body.dns.dkim.value).to.eql('v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
expect(res.body.dns.dkim.expected).to.eql('v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
expect(res.body.dns.dkim.value).to.eql('"v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain) + '"');
expect(res.body.dns.dkim.expected).to.eql('"v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain) + '"');
expect(res.body.dns.dkim.status).to.eql(true);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.value).to.eql('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
expect(res.body.dns.spf.status).to.eql(true);
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.expected).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.dmarc.status).to.eql(true);
expect(res.body.dns.dmarc.value).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.value).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(true);
@@ -459,7 +457,7 @@ describe('Mail API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
after(function (done) {
@@ -511,7 +509,7 @@ describe('Mail API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
after(function (done) {
@@ -582,7 +580,7 @@ describe('Mail API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
after(function (done) {
@@ -660,7 +658,7 @@ describe('Mail API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
after(function (done) {
@@ -772,21 +770,17 @@ describe('Mail API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
after(function (done) {
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('set fails if aliases is missing', function (done) {
@@ -902,17 +896,13 @@ describe('Mail API', function () {
});
after(function (done) {
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('add fails without groupId', function (done) {
+8 -4
View File
@@ -167,7 +167,8 @@ describe('OAuth2', function () {
domain: DOMAIN_0.domain,
portBindings: {},
accessRestriction: null,
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var APP_1 = {
@@ -178,7 +179,8 @@ describe('OAuth2', function () {
domain: DOMAIN_0.domain,
portBindings: {},
accessRestriction: { users: [ 'foobar' ] },
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var APP_2 = {
@@ -189,7 +191,8 @@ describe('OAuth2', function () {
domain: DOMAIN_0.domain,
portBindings: {},
accessRestriction: { users: [ USER_0.id ] },
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var APP_3 = {
@@ -200,7 +203,8 @@ describe('OAuth2', function () {
domain: DOMAIN_0.domain,
portBindings: {},
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
// unknown app
+25 -238
View File
@@ -14,262 +14,49 @@ var async = require('async'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var DOMAIN = 'example-server-test.com';
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
config._reset();
config.setFqdn('example-server-test.com');
config.setVersion('1.2.3');
async.series([
server.start,
database._clear
server.start.bind(server),
database._clear,
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
async.series([
database._clear,
server.stop
], done);
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('REST API', function () {
before(setup);
after(cleanup);
it('dns setup fails without provider', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid provider', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'foobar', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with missing domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: '.foo', adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with missing adminFqdn', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid adminFqdn', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my', config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid config', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: 'not an object' })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid zoneName', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: {}, zoneName: 1337 })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid tlsConfig', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: {}, tlsConfig: 'foobar' })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid tlsConfig provider', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: {}, tlsConfig: { provider: 1337 } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
done();
});
});
it('dns setup twice fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(409);
done();
});
});
it('activation fails without username', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('activation fails with invalid username', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: '?this.is-not!valid', password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('activation fails without email', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('activation fails with invalid email', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: 'notanemail' })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('activation fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('activation fails with invalid password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: 'short', email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('activation succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
// stash token for further use
token = result.body.token;
done();
});
});
it('activating twice fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(409);
done();
});
});
it('does not crash with invalid JSON', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
+109 -76
View File
@@ -2,16 +2,19 @@
/* global it:false */
/* global describe:false */
/* global xdescribe:false */
/* global before:false */
/* global after:false */
var async = require('async'),
child_process = require('child_process'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
fs = require('fs'),
nock = require('nock'),
path = require('path'),
paths = require('../../paths.js'),
server = require('../../server.js'),
settings = require('../../settings.js'),
@@ -60,9 +63,9 @@ describe('Settings API', function () {
before(setup);
after(cleanup);
describe('app_autoupdate_pattern', function () {
it('can get app auto update pattern (default)', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
describe('autoupdate_pattern', function () {
it('can get auto update pattern (default)', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -71,8 +74,8 @@ describe('Settings API', function () {
});
});
it('cannot set app_autoupdate_pattern without pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
it('cannot set autoupdate_pattern without pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
@@ -80,13 +83,13 @@ describe('Settings API', function () {
});
});
it('can set app_autoupdate_pattern', function (done) {
it('can set autoupdate_pattern', function (done) {
var eventPattern = null;
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, function (pattern) {
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, function (pattern) {
eventPattern = pattern;
});
superagent.post(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '00 30 11 * * 1-5' })
.end(function (err, res) {
@@ -96,13 +99,13 @@ describe('Settings API', function () {
});
});
it('can set app_autoupdate_pattern to never', function (done) {
it('can set autoupdate_pattern to never', function (done) {
var eventPattern = null;
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, function (pattern) {
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, function (pattern) {
eventPattern = pattern;
});
superagent.post(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: constants.AUTOUPDATE_PATTERN_NEVER })
.end(function (err, res) {
@@ -112,71 +115,8 @@ describe('Settings API', function () {
});
});
it('cannot set invalid app_autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '1 3 x 5 6' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
});
describe('box_autoupdate_pattern', function () {
it('can get app auto update pattern (default)', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.pattern).to.be.ok();
done();
});
});
it('cannot set box_autoupdate_pattern without pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('can set box_autoupdate_pattern', function (done) {
var eventPattern = null;
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, function (pattern) {
eventPattern = pattern;
});
superagent.post(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '00 30 11 * * 1-5' })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(eventPattern === '00 30 11 * * 1-5').to.be.ok();
done();
});
});
it('can set box_autoupdate_pattern to never', function (done) {
var eventPattern = null;
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, function (pattern) {
eventPattern = pattern;
});
superagent.post(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: constants.AUTOUPDATE_PATTERN_NEVER })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(eventPattern).to.eql(constants.AUTOUPDATE_PATTERN_NEVER);
done();
});
});
it('cannot set invalid box_autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
it('cannot set invalid autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '1 3 x 5 6' })
.end(function (err, res) {
@@ -280,6 +220,99 @@ describe('Settings API', function () {
});
});
xdescribe('Certificates API', function () {
var validCert0, validKey0, // example.com
validCert1, validKey1; // *.example.com
before(function () {
child_process.execSync('openssl req -subj "/CN=example.com/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt');
validKey0 = fs.readFileSync('/tmp/server.key', 'utf8');
validCert0 = fs.readFileSync('/tmp/server.crt', 'utf8');
child_process.execSync('openssl req -subj "/CN=*.example.com/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt');
validKey1 = fs.readFileSync('/tmp/server.key', 'utf8');
validCert1 = fs.readFileSync('/tmp/server.crt', 'utf8');
});
it('cannot set certificate without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot set certificate without certificate', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ key: validKey1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate without key', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: validCert1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate with cert not being a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: 1234, key: validKey1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate with key not being a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: validCert1, key: true })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set non wildcard certificate', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: validCert0, key: validKey0 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('can set certificate', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: validCert1, key: validKey1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
});
});
it('did set the certificate', function (done) {
var cert = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf-8');
expect(cert).to.eql(validCert1);
var key = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf-8');
expect(key).to.eql(validKey1);
done();
});
});
describe('time_zone', function () {
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/time_zone')
+14 -3
View File
@@ -20,7 +20,8 @@ var assert = require('assert'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
oauth2 = require('./oauth2.js'),
user = require('../user.js'),
UserError = user.UserError;
UserError = user.UserError,
_ = require('underscore');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -87,7 +88,9 @@ function list(req, res, next) {
user.list(function (error, results) {
if (error) return next(new HttpError(500, error));
var users = results.map(user.removePrivateFields);
var users = results.map(function (result) {
return _.pick(result, 'id', 'username', 'email', 'fallbackEmail', 'displayName', 'groupIds', 'admin');
});
next(new HttpSuccess(200, { users: users }));
});
@@ -103,7 +106,15 @@ function get(req, res, next) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, user.removePrivateFields(result)));
next(new HttpSuccess(200, {
id: result.id,
username: result.username,
displayName: result.displayName,
email: result.email,
fallbackEmail: result.fallbackEmail,
admin: result.admin,
groupIds: result.groupIds
}));
});
}
+27 -44
View File
@@ -24,7 +24,7 @@ function sync(callback) {
callback = callback || NOOP_CALLBACK;
debug('sync: synchronizing global state with installed app state');
debug('Syncing');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
@@ -36,10 +36,11 @@ function sync(callback) {
async.eachSeries(removedAppIds, function (appId, iteratorDone) {
stopJobs(appId, gState[appId], iteratorDone);
}, function (error) {
if (error) debug('sync: error stopping jobs of removed apps', error);
if (error) debug('Error stopping jobs of removed apps', error);
gState = _.omit(gState, removedAppIds);
debug('sync: checking apps %j', allAppIds);
async.eachSeries(allApps, function (app, iteratorDone) {
var appState = gState[app.id] || null;
var schedulerConfig = app.manifest.addons ? app.manifest.addons.scheduler : null;
@@ -50,10 +51,9 @@ function sync(callback) {
return iteratorDone(); // nothing changed
}
debug(`sync: app ${app.fqdn} changed`);
debug('sync: app %s changed', app.id);
stopJobs(app.id, appState, function (error) {
if (error) debug(`sync: error stopping jobs of ${app.fqdn} : ${error.message}`);
if (error) debug('Error stopping jobs for %s : %s', app.id, error.message);
if (!schedulerConfig) {
delete gState[app.id];
@@ -62,21 +62,20 @@ function sync(callback) {
gState[app.id] = {
schedulerConfig: schedulerConfig,
cronJobs: createCronJobs(app, schedulerConfig)
cronJobs: createCronJobs(app.id, schedulerConfig)
};
iteratorDone();
});
});
debug('sync: done');
debug('Done syncing');
});
});
}
function killContainer(containerName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof callback, 'function');
if (!containerName) return callback();
async.series([
docker.stopContainerByName.bind(null, containerName),
@@ -93,7 +92,7 @@ function stopJobs(appId, appState, callback) {
assert.strictEqual(typeof appState, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`stopJobs: stopping jobs of ${appId}`);
debug('stopJobs for %s', appId);
if (!appState) return callback();
@@ -102,30 +101,29 @@ function stopJobs(appId, appState, callback) {
appState.cronJobs[taskName].stop();
}
killContainer(`${appId}-${taskName}`, iteratorDone);
var containerName = appId + '-' + taskName;
killContainer(containerName, iteratorDone);
}, callback);
}
function createCronJobs(app, schedulerConfig) {
assert.strictEqual(typeof app, 'object');
function createCronJobs(appId, schedulerConfig) {
assert.strictEqual(typeof appId, 'string');
assert(schedulerConfig && typeof schedulerConfig === 'object');
debug(`createCronJobs: creating cron jobs for app ${app.fqdn}`);
debug('creating cron jobs for app %s', appId);
var jobs = { };
Object.keys(schedulerConfig).forEach(function (taskName) {
var task = schedulerConfig[taskName];
const randomSecond = Math.floor(60*Math.random()); // don't start all crons to decrease memory pressure
var cronTime = (config.TEST ? '*/5 ' : '00 ') + task.schedule; // time ticks faster in tests
var cronTime = (config.TEST ? '*/5 ' : `${randomSecond} `) + task.schedule; // time ticks faster in tests
debug(`createCronJobs: ${app.fqdn} task ${taskName} scheduled at ${cronTime} with cmd ${task.command}`);
debug('scheduling task for %s/%s @ %s : %s', appId, taskName, cronTime, task.command);
var cronJob = new CronJob({
cronTime: cronTime, // at this point, the pattern has been validated
onTick: runTask.bind(null, app.id, taskName), // put the app id in closure, so we don't use the outdated app object by mistake
onTick: doTask.bind(null, appId, taskName),
start: true
});
@@ -135,50 +133,35 @@ function createCronJobs(app, schedulerConfig) {
return jobs;
}
function runTask(appId, taskName, callback) {
function doTask(appId, taskName, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskName, 'string');
assert(!callback || typeof callback === 'function');
const JOB_MAX_TIME = 30 * 60 * 1000; // 30 minutes
callback = callback || NOOP_CALLBACK;
debug(`runTask: running task ${taskName} of ${appId}`);
debug('Executing task %s/%s', appId, taskName);
apps.get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING || app.health !== appdb.HEALTH_HEALTHY) {
debug(`runTask: skipped task ${taskName} because app ${app.fqdn} has run state ${app.installationState}`);
debug('task %s skipped. app %s is not installed/running/healthy', taskName, app.id);
return callback();
}
const containerName = `${app.id}-${taskName}`;
var containerName = app.id + '-' + taskName;
docker.inspectByName(containerName, function (err, data) {
if (!err && data && data.State.Running === true) {
const jobStartTime = new Date(data.State.StartedAt); // iso 8601
if (new Date() - jobStartTime < JOB_MAX_TIME) {
debug(`runTask: skipped task ${taskName} of app ${app.fqdn} since it was started at ${jobStartTime}`);
return callback();
}
}
killContainer(containerName, function (error) {
if (error) return callback(error);
debug(`runTask: removing any old task ${taskName} of app ${app.fqdn}`);
debug('Creating subcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
killContainer(containerName, function (error) {
// NOTE: if you change container name here, fix addons.js to return correct container names
docker.createSubcontainer(app, containerName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], { } /* options */, function (error, container) {
if (error) return callback(error);
const cmd = gState[appId].schedulerConfig[taskName].command;
debug(`runTask: starting task ${taskName} of app ${app.fqdn} with cmd ${cmd}`);
// NOTE: if you change container name here, fix addons.js to return correct container names
docker.createSubcontainer(app, containerName, [ '/bin/sh', '-c', cmd ], { } /* options */, function (error, container) {
if (error) return callback(error);
docker.startContainer(container.id, callback);
});
docker.startContainer(container.id, callback);
});
});
});
+3 -8
View File
@@ -60,9 +60,7 @@ function initializeExpressSync() {
router.del = router.delete; // amend router.del for readability further on
app
// the timeout middleware will respond with a 503. the request itself cannot be 'aborted' and will continue
// search for req.clearTimeout in route handlers to see places where this timeout is reset
.use(middleware.timeout(REQUEST_TIMEOUT, { respond: true }))
.use(middleware.timeout(REQUEST_TIMEOUT))
.use(json)
.use(urlencoded)
.use(middleware.cookieParser())
@@ -197,10 +195,8 @@ function initializeExpressSync() {
router.post('/api/v1/apps/:id/upload', appsScope, routes.user.requireAdmin, multipart, routes.apps.uploadFile);
// settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above)
router.get ('/api/v1/settings/app_autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getAppAutoupdatePattern);
router.post('/api/v1/settings/app_autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.setAppAutoupdatePattern);
router.get ('/api/v1/settings/box_autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getBoxAutoupdatePattern);
router.post('/api/v1/settings/box_autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.setBoxAutoupdatePattern);
router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getAutoupdatePattern);
router.post('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.setAutoupdatePattern);
router.get ('/api/v1/settings/cloudron_name', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronName);
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.user.requireAdmin, routes.settings.setCloudronName);
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronAvatar);
@@ -215,7 +211,6 @@ function initializeExpressSync() {
// email routes
router.get ('/api/v1/mail/:domain', settingsScope, routes.user.requireAdmin, routes.mail.get);
router.post('/api/v1/mail/:domain', settingsScope, routes.user.requireAdmin, routes.mail.update);
router.post('/api/v1/mail', settingsScope, routes.user.requireAdmin, routes.mail.add);
router.del ('/api/v1/mail/:domain', settingsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.mail.del);
router.get ('/api/v1/mail/:domain/status', settingsScope, routes.user.requireAdmin, routes.mail.getStatus);
+10 -44
View File
@@ -6,11 +6,8 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
getAppAutoupdatePattern: getAppAutoupdatePattern,
setAppAutoupdatePattern: setAppAutoupdatePattern,
getBoxAutoupdatePattern: getBoxAutoupdatePattern,
setBoxAutoupdatePattern: setBoxAutoupdatePattern,
getAutoupdatePattern: getAutoupdatePattern,
setAutoupdatePattern: setAutoupdatePattern,
getTimeZone: getTimeZone,
setTimeZone: setTimeZone,
@@ -48,8 +45,7 @@ exports = module.exports = {
CAAS_CONFIG_KEY: 'caas_config',
// strings
APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern',
BOX_AUTOUPDATE_PATTERN_KEY: 'box_autoupdate_pattern',
AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern',
TIME_ZONE_KEY: 'time_zone',
CLOUDRON_NAME_KEY: 'cloudron_name',
@@ -73,8 +69,7 @@ var assert = require('assert'),
var gDefaults = (function () {
var result = { };
result[exports.APP_AUTOUPDATE_PATTERN_KEY] = '00 30 1,3,5,23 * * *';
result[exports.BOX_AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
result[exports.AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DYNAMIC_DNS_KEY] = false;
@@ -130,7 +125,7 @@ function uninitialize(callback) {
callback();
}
function setAppAutoupdatePattern(pattern, callback) {
function setAutoupdatePattern(pattern, callback) {
assert.strictEqual(typeof pattern, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -139,49 +134,20 @@ function setAppAutoupdatePattern(pattern, callback) {
if (!job) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Invalid pattern'));
}
settingsdb.set(exports.APP_AUTOUPDATE_PATTERN_KEY, pattern, function (error) {
settingsdb.set(exports.AUTOUPDATE_PATTERN_KEY, pattern, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.APP_AUTOUPDATE_PATTERN_KEY, pattern);
exports.events.emit(exports.AUTOUPDATE_PATTERN_KEY, pattern);
return callback(null);
});
}
function getAppAutoupdatePattern(callback) {
function getAutoupdatePattern(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.APP_AUTOUPDATE_PATTERN_KEY, function (error, pattern) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.APP_AUTOUPDATE_PATTERN_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, pattern);
});
}
function setBoxAutoupdatePattern(pattern, callback) {
assert.strictEqual(typeof pattern, 'string');
assert.strictEqual(typeof callback, 'function');
if (pattern !== constants.AUTOUPDATE_PATTERN_NEVER) { // check if pattern is valid
var job = safe.safeCall(function () { return new CronJob(pattern); });
if (!job) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Invalid pattern'));
}
settingsdb.set(exports.BOX_AUTOUPDATE_PATTERN_KEY, pattern, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.BOX_AUTOUPDATE_PATTERN_KEY, pattern);
return callback(null);
});
}
function getBoxAutoupdatePattern(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.BOX_AUTOUPDATE_PATTERN_KEY, function (error, pattern) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.BOX_AUTOUPDATE_PATTERN_KEY]);
settingsdb.get(exports.AUTOUPDATE_PATTERN_KEY, function (error, pattern) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.AUTOUPDATE_PATTERN_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, pattern);
+37 -21
View File
@@ -119,18 +119,38 @@ function configureWebadmin(callback) {
gWebadminStatus.configuring = true; // re-entracy guard
function done(error) {
gWebadminStatus.configuring = false;
debug('configureWebadmin: done error: %j', error || {});
callback(error);
}
function configureReverseProxy(error) {
debug('configureReverseProxy: error %j', error || null);
reverseProxy.configureAdmin({ userId: null, username: 'setup' }, function (error) {
debug('configureWebadmin: done error: %j', error || {});
gWebadminStatus.configuring = false;
if (error) return callback(error);
if (error) return done(error);
gWebadminStatus.tls = true;
callback();
done();
});
}
function addWebadminDnsRecord(ip, domain, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test') return callback();
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
domains.upsertDNSRecords(config.adminLocation(), domain, 'A', [ ip ], retryCallback);
}, function (error) {
if (error) debug('addWebadminDnsRecord: done updating records with error:', error);
else debug('addWebadminDnsRecord: done');
callback(error);
});
}
@@ -139,11 +159,10 @@ function configureWebadmin(callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return configureReverseProxy(error);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
debug('addWebadminDnsRecord: updated records with error:', error);
addWebadminDnsRecord(ip, config.adminDomain(), function (error) {
if (error) return configureReverseProxy(error);
domains.waitForDnsRecord(config.adminFqdn(), config.adminDomain(), ip, { interval: 30000, times: 50000 }, function (error) {
domains.waitForDNSRecord(config.adminFqdn(), config.adminDomain(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return configureReverseProxy(error);
gWebadminStatus.dns = true;
@@ -167,11 +186,9 @@ function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, c
if (gWebadminStatus.configuring || gWebadminStatus.restoring) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
if (!tld.isValid(adminFqdn) || !adminFqdn.endsWith(domain)) return callback(new SetupError(SetupError.BAD_FIELD, 'adminFqdn must be a subdomain of domain'));
if (!zoneName) zoneName = tld.getDomain(domain) || domain;
debug(`dnsSetup: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
debug('dnsSetup: Setting up Cloudron with domain %s and zone %s', domain, zoneName);
function done(error) {
if (error && error.reason === DomainError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
@@ -193,12 +210,14 @@ function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, c
domains.get(domain, function (error, result) {
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (result) return callback(new SettingsError(SettingsError.ALREADY_EXISTS, 'domain already exists'));
async.series([
domains.add.bind(null, domain, zoneName, provider, dnsConfig, null /* cert */, tlsConfig),
mail.add.bind(null, domain)
], done);
if (!result) {
async.series([
domains.add.bind(null, domain, zoneName, provider, dnsConfig, null /* cert */, tlsConfig),
mail.add.bind(null, domain)
], done);
} else {
domains.update(domain, provider, dnsConfig, null /* cert */, tlsConfig, done);
}
});
}
@@ -285,7 +304,7 @@ function restore(backupConfig, backupId, version, callback) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new SetupError(SetupError.EXTERNAL_ERROR, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider} with format ${backupConfig.format}`);
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider}`);
gWebadminStatus.restoring = true;
@@ -294,9 +313,6 @@ function restore(backupConfig, backupId, version, callback) {
async.series([
backups.restore.bind(null, backupConfig, backupId),
autoprovision,
// currently, our suggested restore flow is after a dnsSetup. This re-creates DKIM keys and updates the DNS
// for this reason, we have to re-setup DNS after a restore. Once we have a 100% IP based restore, we can skip this
mail.addDnsRecords.bind(null, config.adminDomain()),
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
], function (error) {
debug('restore:', error);
+1 -1
View File
@@ -204,7 +204,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
var relativePath = path.relative(oldFilePath, file.name);
file.copy(path.join(newFilePath, relativePath), function(error) {
if (error && error.code === 404) return iteratorCallback(new BackupsError(BackupsError.NOT_FOUND, 'Old backup not found'));
if (error && error.code == 404) return iteratorCallback(new BackupsError(BackupsError.NOT_FOUND, 'Old backup not found'));
if (error) {
debug('copyBackup: gcs copy error', error);
return iteratorCallback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
-6
View File
@@ -6,13 +6,8 @@
// New backends can start from here
// -------------------------------------------
// Implementation note:
// retry logic for upload() comes from the syncer since it is stream based
// for the other API calls we leave it to the backend to retry. this allows
// them to tune the concurrency based on failures/rate limits accordingly
exports = module.exports = {
upload: upload,
download: download,
downloadDir: downloadDir,
copy: copy,
@@ -35,7 +30,6 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
assert.strictEqual(typeof callback, 'function');
// Result: none
// sourceStream errors are handled upstream
callback(new Error('not implemented'));
}
+52 -55
View File
@@ -72,10 +72,7 @@ function getCaasConfig(apiConfig, callback) {
region: apiConfig.region || 'us-east-1',
maxRetries: 5,
retryDelayOptions: {
customBackoff: () => 20000 // constant backoff - https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#retryDelayOptions-property
},
httpOptions: {
connectTimeout: 10000 // https://github.com/aws/aws-sdk-js/pull/1446
base: 20000 // 2^5 * 20 seconds
}
};
@@ -104,17 +101,16 @@ function getS3Config(apiConfig, callback) {
region: apiConfig.region || 'us-east-1',
maxRetries: 5,
retryDelayOptions: {
customBackoff: () => 20000 // constant backoff - https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#retryDelayOptions-property
},
httpOptions: {
connectTimeout: 10000 // https://github.com/aws/aws-sdk-js/pull/1446
base: 20000 // 2^5 * 20 seconds
}
};
if (apiConfig.endpoint) credentials.endpoint = apiConfig.endpoint;
if (apiConfig.acceptSelfSignedCerts === true && credentials.endpoint && credentials.endpoint.startsWith('https://')) {
credentials.httpOptions.agent = new https.Agent({ rejectUnauthorized: false });
credentials.httpOptions.agent = {
agent: new https.Agent({ rejectUnauthorized: false })
};
}
callback(null, credentials);
}
@@ -126,6 +122,15 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
assert.strictEqual(typeof sourceStream, 'object');
assert.strictEqual(typeof callback, 'function');
function done(error) {
if (error) {
debug('[%s] upload: s3 upload error.', backupFilePath, error);
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Error uploading ${backupFilePath}. Message: ${error.message} HTTP Code: ${error.code}`));
}
callback(null);
}
getS3Config(apiConfig, function (error, credentials) {
if (error) return callback(error);
@@ -139,14 +144,7 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
// s3.upload automatically does a multi-part upload. we set queueSize to 1 to reduce memory usage
// uploader will buffer at most queueSize * partSize bytes into memory at any given time.
s3.upload(params, { partSize: 10 * 1024 * 1024, queueSize: 1 }, function (error) {
if (error) {
debug('[%s] upload: s3 upload error.', backupFilePath, error);
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Error uploading ${backupFilePath}. Message: ${error.message} HTTP Code: ${error.code}`));
}
callback(null);
});
return s3.upload(params, { partSize: 10 * 1024 * 1024, queueSize: 1 }, done);
});
}
@@ -183,14 +181,14 @@ function download(apiConfig, backupFilePath, callback) {
});
}
function listDir(apiConfig, dir, iteratorCallback, callback) {
function listDir(apiConfig, backupFilePath, iteratorCallback, callback) {
getS3Config(apiConfig, function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var listParams = {
Bucket: apiConfig.bucket,
Prefix: dir
Prefix: backupFilePath
};
async.forever(function listAndDownload(foreverCallback) {
@@ -309,28 +307,24 @@ function copy(apiConfig, oldFilePath, newFilePath) {
};
// S3 copyObject has a file size limit of 5GB so if we have larger files, we do a multipart copy
// Exoscale takes too long to copy 5GB
const largeFileLimit = apiConfig.provider === 'exoscale-sos' ? 1024 * 1024 * 1024 : 5 * 1024 * 1024 * 1024;
if (content.Size < largeFileLimit) {
events.emit('progress', `Copying ${relativePath || oldFilePath}`);
if (content.Size < 5 * 1024 * 1024 * 1024 || apiConfig.provider === 'digitalocean-spaces') { // DO has not implemented this yet
events.emit('progress', `Copying ${relativePath}`);
copyParams.CopySource = encodeCopySource(apiConfig.bucket, content.Key);
s3.copyObject(copyParams, done).on('retry', function (response) {
++retryCount;
events.emit('progress', `Retrying (${response.retryCount+1}) copy of ${relativePath || oldFilePath}. Status code: ${response.httpResponse.statusCode}`);
events.emit('progress', `Retrying (${response.retryCount+1}) copy of ${relativePath}. Status code: ${response.httpResponse.statusCode}`);
});
return;
}
events.emit('progress', `Copying (multipart) ${relativePath || oldFilePath}`);
events.emit('progress', `Copying (multipart) ${relativePath}`);
s3.createMultipartUpload(copyParams, function (error, result) {
if (error) return done(error);
// Exoscale (96M) was suggested by exoscale. 1GB - rather random size for others
const chunkSize = apiConfig.provider === 'exoscale-sos' ? 96 * 1024 * 1024 : 1024 * 1024 * 1024;
const CHUNK_SIZE = 1024 * 1024 * 1024; // 1GB - rather random size
var uploadId = result.UploadId;
var uploadedParts = [];
var partNumber = 1;
@@ -339,7 +333,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
var size = content.Size-1;
function copyNextChunk() {
endBytes = startBytes + chunkSize;
endBytes = startBytes + CHUNK_SIZE;
if (endBytes > size) endBytes = size;
var params = {
@@ -372,7 +366,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
s3.completeMultipartUpload(params, done);
}).on('retry', function (response) {
++retryCount;
events.emit('progress', `Retrying (${response.retryCount+1}) multipart copy of ${relativePath || oldFilePath}. Status code: ${response.httpResponse.statusCode}`);
events.emit('progress', `Retrying (${response.retryCount+1}) multipart copy of ${relativePath}. Status code: ${response.httpResponse.statusCode}`);
});
}
@@ -382,11 +376,11 @@ function copy(apiConfig, oldFilePath, newFilePath) {
var total = 0, concurrency = 4;
listDir(apiConfig, oldFilePath, function listDirIterator(s3, objects, done) {
listDir(apiConfig, oldFilePath, function (s3, objects, done) {
total += objects.length;
if (retryCount === 0) concurrency = Math.min(concurrency + 1, 10); else concurrency = Math.max(concurrency - 1, 5);
events.emit('progress', `${retryCount} errors so far. concurrency set to ${concurrency}`);
events.emit('progress', `${retryCount} errors. concurrency set to ${concurrency}`);
retryCount = 0;
async.eachLimit(objects, concurrency, copyFile.bind(null, s3), done);
@@ -416,11 +410,10 @@ function remove(apiConfig, filename, callback) {
}
};
// deleteObjects does not return error if key is not found
s3.deleteObjects(deleteParams, function (error) {
if (error) debug(`remove: Unable to remove ${deleteParams.Key}. error: ${error.message}`);
if (error) debug('remove: Unable to remove %s. Not fatal.', deleteParams.Key, error);
callback(error);
callback(null);
});
});
}
@@ -432,29 +425,33 @@ function removeDir(apiConfig, pathPrefix) {
var events = new EventEmitter();
var total = 0;
listDir(apiConfig, pathPrefix, function listDirIterator(s3, objects, done) {
function deleteFiles(s3, contents, iteratorCallback) {
var deleteParams = {
Bucket: apiConfig.bucket,
Delete: {
Objects: contents.map(function (c) { return { Key: c.Key }; })
}
};
events.emit('progress', `Removing ${contents.length} files from ${contents[0].Key} to ${contents[contents.length-1].Key}`);
s3.deleteObjects(deleteParams, function (error /*, deleteData */) {
if (error) {
events.emit('progress', `Unable to remove ${deleteParams.Key} ${error.message}`);
return iteratorCallback(error);
}
iteratorCallback();
});
}
listDir(apiConfig, pathPrefix, function (s3, objects, done) {
total += objects.length;
const chunkSize = apiConfig.provider !== 'digitalocean-spaces' ? 1000 : 100; // throttle objects in each request
var chunks = chunk(objects, chunkSize);
const batchSize = apiConfig.provider !== 'digitalocean-spaces' ? 1000 : 100; // throttle objects in each request
var chunks = batchSize === 1 ? objects : chunk(objects, batchSize);
async.eachSeries(chunks, function deleteFiles(contents, iteratorCallback) {
var deleteParams = {
Bucket: apiConfig.bucket,
Delete: {
Objects: contents.map(function (c) { return { Key: c.Key }; })
}
};
events.emit('progress', `Removing ${contents.length} files from ${contents[0].Key} to ${contents[contents.length-1].Key}`);
// deleteObjects does not return error if key is not found
s3.deleteObjects(deleteParams, function (error /*, deleteData */) {
if (error) events.emit('progress', `Unable to remove ${deleteParams.Key} ${error.message}`);
iteratorCallback(error);
});
}, done);
async.eachSeries(chunks, deleteFiles.bind(null, s3), done);
}, function (error) {
events.emit('progress', `Removed ${total} files`);
+2 -1
View File
@@ -16,6 +16,7 @@ var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
child_process = require('child_process'),
config = require('./config.js'),
debug = require('debug')('box:taskmanager'),
locker = require('./locker.js'),
sendFailureLogs = require('./logcollector.js').sendFailureLogs,
@@ -47,7 +48,7 @@ function resumeTasks(callback) {
if (app.installationState === appdb.ISTATE_ERROR) return;
debug('Creating process for %s (%s) with state %s', app.fqdn, app.id, app.installationState);
debug('Creating process for %s (%s) with state %s', app.intrinsicFqdn, app.id, app.installationState);
restartAppTask(app.id, NOOP_CALLBACK); // restart because the auto-installer could have queued up tasks already
});
-176
View File
@@ -1,176 +0,0 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
/* global beforeEach:false */
'use strict';
var async = require('async'),
appstore = require('../appstore.js'),
AppstoreError = appstore.AppstoreError,
config = require('../config.js'),
database = require('../database.js'),
expect = require('expect.js'),
nock = require('nock'),
settings = require('../settings.js');
const DOMAIN = 'example-appstore-test.com';
const APPSTORE_USER_ID = 'appstoreuserid';
const APPSTORE_TOKEN = 'appstoretoken';
const CLOUDRON_ID = 'cloudronid';
const APP_ID = 'appid';
const APPSTORE_APP_ID = 'appstoreappid';
function setup(done) {
nock.cleanAll();
config.setFqdn(DOMAIN);
config.setAdminFqdn('my.' + DOMAIN);
async.series([
database.initialize,
database._clear,
settings.initialize
], done);
}
function cleanup(done) {
nock.cleanAll();
async.series([
settings.uninitialize,
database._clear,
database.uninitialize
], done);
}
describe('Appstore', function () {
before(setup);
after(cleanup);
beforeEach(nock.cleanAll);
it('cannot send alive status without appstore config', function (done) {
appstore.sendAliveStatus(function (error) {
expect(error).to.be.ok();
expect(error.reason).to.equal(AppstoreError.BILLING_REQUIRED);
done();
});
});
it('can set appstore config', function (done) {
var scope = nock('http://localhost:6060')
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(201, { cloudron: { id: CLOUDRON_ID }});
settings.setAppstoreConfig({ userId: APPSTORE_USER_ID, token: APPSTORE_TOKEN }, function (error) {
expect(error).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
done();
});
});
it('can send alive status', function (done) {
var scope = nock('http://localhost:6060')
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/alive?accessToken=${APPSTORE_TOKEN}`, function (body) {
expect(body.version).to.be.a('string');
expect(body.adminFqdn).to.be.a('string');
expect(body.provider).to.be.a('string');
expect(body.backendSettings).to.be.an('object');
expect(body.backendSettings.backupConfig).to.be.an('object');
expect(body.backendSettings.backupConfig.provider).to.be.a('string');
expect(body.backendSettings.backupConfig.hardlinks).to.be.a('boolean');
expect(body.backendSettings.domainConfig).to.be.an('object');
expect(body.backendSettings.domainConfig.count).to.be.a('number');
expect(body.backendSettings.domainConfig.domains).to.be.an('array');
expect(body.backendSettings.mailConfig).to.be.an('object');
expect(body.backendSettings.mailConfig.outboundCount).to.be.a('number');
expect(body.backendSettings.mailConfig.inboundCount).to.be.a('number');
expect(body.backendSettings.mailConfig.catchAllCount).to.be.a('number');
expect(body.backendSettings.mailConfig.relayProviders).to.be.an('array');
expect(body.backendSettings.appAutoupdatePattern).to.be.a('string');
expect(body.backendSettings.boxAutoupdatePattern).to.be.a('string');
expect(body.backendSettings.timeZone).to.be.a('string');
expect(body.machine).to.be.an('object');
expect(body.machine.cpus).to.be.an('array');
expect(body.machine.totalmem).to.be.an('number');
expect(body.events).to.be.an('object');
expect(body.events.lastLogin).to.be.an('number');
return true;
})
.reply(201, { cloudron: { id: CLOUDRON_ID }});
appstore.sendAliveStatus(function (error) {
expect(error).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
done();
});
});
it('can get account', function (done) {
var scope = nock('http://localhost:6060')
.get(`/api/v1/users/${APPSTORE_USER_ID}?accessToken=${APPSTORE_TOKEN}`)
.reply(200, { profile: { id: APPSTORE_USER_ID }});
appstore.getAccount(function (error, result) {
expect(error).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
expect(result.id).to.equal(APPSTORE_USER_ID);
done();
});
});
it('can purchase an app', function (done) {
var scope = nock('http://localhost:6060')
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(201, {});
appstore.purchase(APP_ID, APPSTORE_APP_ID, function (error) {
expect(error).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
done();
});
});
it('unpurchase succeeds if app was never purchased', function (done) {
var scope1 = nock('http://localhost:6060')
.get(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`)
.reply(404, {});
var scope2 = nock('http://localhost:6060')
.delete(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(204, {});
appstore.unpurchase(APP_ID, APPSTORE_APP_ID, function (error) {
expect(error).to.not.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.not.be.ok();
done();
});
});
it('can unpurchase an app', function (done) {
var scope1 = nock('http://localhost:6060')
.get(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`)
.reply(200, {});
var scope2 = nock('http://localhost:6060')
.delete(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(204, {});
appstore.unpurchase(APP_ID, APPSTORE_APP_ID, function (error) {
expect(error).to.not.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done();
});
});
});
+1
View File
@@ -68,6 +68,7 @@ var APP = {
runState: null,
location: 'applocation',
domain: DOMAIN_0.domain,
intrinsicFqdn: DOMAIN_0.domain + '.' + 'applocation',
fqdn: DOMAIN_0.domain + '.' + 'applocation',
manifest: MANIFEST,
containerId: null,
+8 -8
View File
@@ -210,6 +210,7 @@ describe('database', function () {
oldConfig: null,
newConfig: null,
memoryLimit: 4294967296,
altDomain: null,
xFrameOptions: 'DENY',
sso: true,
debugMode: null,
@@ -715,6 +716,7 @@ describe('database', function () {
oldConfig: null,
updateConfig: null,
memoryLimit: 4294967296,
altDomain: null,
xFrameOptions: 'DENY',
sso: true,
debugMode: null,
@@ -741,6 +743,7 @@ describe('database', function () {
oldConfig: null,
updateConfig: null,
memoryLimit: 0,
altDomain: null,
xFrameOptions: 'SAMEORIGIN',
sso: true,
debugMode: null,
@@ -1362,7 +1365,7 @@ describe('database', function () {
});
it('getAllPaged succeeds', function (done) {
eventlogdb.getAllPaged([], null, 1, 1, function (error, results) {
eventlogdb.getAllPaged(null, null, 1, 1, function (error, results) {
expect(error).to.be(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
@@ -1377,7 +1380,7 @@ describe('database', function () {
});
it('getAllPaged succeeds with source search', function (done) {
eventlogdb.getAllPaged([], '1.2.3.4', 1, 1, function (error, results) {
eventlogdb.getAllPaged(null, '1.2.3.4', 1, 1, function (error, results) {
expect(error).to.be(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
@@ -1392,7 +1395,7 @@ describe('database', function () {
});
it('getAllPaged succeeds with data search', function (done) {
eventlogdb.getAllPaged([], 'thatapp', 1, 1, function (error, results) {
eventlogdb.getAllPaged(null, 'thatapp', 1, 1, function (error, results) {
expect(error).to.be(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
@@ -1417,7 +1420,7 @@ describe('database', function () {
eventlogdb.delByCreationTime(new Date(), actions, function (error) {
expect(error).to.be(null);
eventlogdb.getAllPaged([], null, 1, 100, function (error, results) {
eventlogdb.getAllPaged(null, null, 1, 100, function (error, results) {
expect(error).to.be(null);
expect(results.length).to.be(2);
results = results.sort(function (x, y) { return x.action > y.action; }); // because equal timestamp gives random ordering
@@ -1590,10 +1593,7 @@ describe('database', function () {
describe('mailboxes', function () {
before(function (done) {
async.series([
domaindb.add.bind(null, DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }),
maildb.add.bind(null, DOMAIN_0.domain)
], done);
domaindb.add(DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }, done);
});
after(function (done) {
+18 -18
View File
@@ -55,7 +55,7 @@ describe('dns provider', function () {
});
it('upsert succeeds', function (done) {
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('noop-record-id');
@@ -64,7 +64,7 @@ describe('dns provider', function () {
});
it('get succeeds', function (done) {
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
domains.getDNSRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.an(Array);
expect(result.length).to.eql(0);
@@ -74,7 +74,7 @@ describe('dns provider', function () {
});
it('del succeeds', function (done) {
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) {
domains.removeDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) {
expect(error).to.eql(null);
done();
@@ -115,7 +115,7 @@ describe('dns provider', function () {
.post('/v2/domains/' + DOMAIN_0.zoneName + '/records')
.reply(201, { domain_record: DOMAIN_RECORD_0 });
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('3352892');
expect(req1.isDone()).to.be.ok();
@@ -165,7 +165,7 @@ describe('dns provider', function () {
.put('/v2/domains/' + DOMAIN_0.zoneName + '/records/' + DOMAIN_RECORD_1.id)
.reply(200, { domain_record: DOMAIN_RECORD_1_NEW });
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ DOMAIN_RECORD_1_NEW.data ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ DOMAIN_RECORD_1_NEW.data ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('3352893');
expect(req1.isDone()).to.be.ok();
@@ -251,7 +251,7 @@ describe('dns provider', function () {
.post('/v2/domains/' + DOMAIN_0.zoneName + '/records')
.reply(201, { domain_record: DOMAIN_RECORD_2_NEW });
domains.upsertDnsRecords('', DOMAIN_0.domain, 'TXT', [ DOMAIN_RECORD_2_NEW.data, DOMAIN_RECORD_1_NEW.data, DOMAIN_RECORD_3_NEW.data ], function (error, result) {
domains.upsertDNSRecords('', DOMAIN_0.domain, 'TXT', [ DOMAIN_RECORD_2_NEW.data, DOMAIN_RECORD_1_NEW.data, DOMAIN_RECORD_3_NEW.data ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('3352893');
expect(req1.isDone()).to.be.ok();
@@ -290,7 +290,7 @@ describe('dns provider', function () {
.get('/v2/domains/' + DOMAIN_0.zoneName + '/records')
.reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1 ] });
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
domains.getDNSRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.an(Array);
expect(result.length).to.eql(1);
@@ -331,7 +331,7 @@ describe('dns provider', function () {
.delete('/v2/domains/' + DOMAIN_0.zoneName + '/records/' + DOMAIN_RECORD_1.id)
.reply(204, {});
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
domains.removeDNSRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
@@ -437,7 +437,7 @@ describe('dns provider', function () {
}
}]);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('/change/C2QLKQIWEI0BZF');
expect(awsAnswerQueue.length).to.eql(0);
@@ -456,7 +456,7 @@ describe('dns provider', function () {
}
}]);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('/change/C2QLKQIWEI0BZF');
expect(awsAnswerQueue.length).to.eql(0);
@@ -475,7 +475,7 @@ describe('dns provider', function () {
}
}]);
domains.upsertDnsRecords('', DOMAIN_0.domain, 'TXT', [ 'first', 'second', 'third' ], function (error, result) {
domains.upsertDNSRecords('', DOMAIN_0.domain, 'TXT', [ 'first', 'second', 'third' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('/change/C2QLKQIWEI0BZF');
expect(awsAnswerQueue.length).to.eql(0);
@@ -496,7 +496,7 @@ describe('dns provider', function () {
}]
}]);
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
domains.getDNSRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.an(Array);
expect(result.length).to.eql(1);
@@ -517,7 +517,7 @@ describe('dns provider', function () {
}
}]);
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
domains.removeDNSRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(awsAnswerQueue.length).to.eql(0);
@@ -588,7 +588,7 @@ describe('dns provider', function () {
zoneQueue.push([null, [ ]]); // getRecords
zoneQueue.push([null, {id: '1'}]);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('1');
expect(zoneQueue.length).to.eql(0);
@@ -602,7 +602,7 @@ describe('dns provider', function () {
zoneQueue.push([null, [GCDNS().zone('test').record('A', {'name': 'test', data:['5.6.7.8'], ttl: 1})]]);
zoneQueue.push([null, {id: '2'}]);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('2');
expect(zoneQueue.length).to.eql(0);
@@ -616,7 +616,7 @@ describe('dns provider', function () {
zoneQueue.push([null, [ ]]); // getRecords
zoneQueue.push([null, {id: '3'}]);
domains.upsertDnsRecords('', DOMAIN_0.domain, 'TXT', [ 'first', 'second', 'third' ], function (error, result) {
domains.upsertDNSRecords('', DOMAIN_0.domain, 'TXT', [ 'first', 'second', 'third' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('3');
expect(zoneQueue.length).to.eql(0);
@@ -629,7 +629,7 @@ describe('dns provider', function () {
zoneQueue.push([null, HOSTED_ZONES]);
zoneQueue.push([null, [GCDNS().zone('test').record('A', {'name': 'test', data:['1.2.3.4', '5.6.7.8'], ttl: 1})]]);
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
domains.getDNSRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.an(Array);
expect(result.length).to.eql(2);
@@ -645,7 +645,7 @@ describe('dns provider', function () {
zoneQueue.push([null, [GCDNS().zone('test').record('A', {'name': 'test', data:['5.6.7.8'], ttl: 1})]]);
zoneQueue.push([null, {id: '5'}]);
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
domains.removeDNSRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(zoneQueue.length).to.eql(0);
+1 -1
View File
@@ -69,7 +69,7 @@ describe('Eventlog', function () {
});
it('getAllPaged succeeds', function (done) {
eventlog.getAllPaged([], null, 1, 1, function (error, results) {
eventlog.getAllPaged(null, null, 1, 1, function (error, results) {
expect(error).to.be(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
+36 -11
View File
@@ -58,12 +58,6 @@ describe('Certificates', function () {
var validCert2 = '-----BEGIN CERTIFICATE-----\nMIIBwjCCAWwCCQCZjm6jL50XfTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wHhcN\nMTYxMTA4MDgyMDE1WhcNMjAxMTA3MDgyMDE1WjBoMQswCQYDVQQGEwJERTEPMA0G\nA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05lYnVsb24x\nDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEAtKoyTPrf2DjKbnW7Xr1HbRvV+quHTcGmUq5anDI7G4w/\nabqDXGYyakHHlPyZxYp7FWQxCm83rHUuDT1LiLIBZQIDAQABMA0GCSqGSIb3DQEB\nCwUAA0EAVaD2Q6bF9hcUUBev5NyjaMdDYURuWfjuwWUkb8W50O2ed3O+MATKrDdS\nyVaBy8W02KJ4Y1ym4je/MF8nilPurA==\n-----END CERTIFICATE-----';
var validKey2 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBPQIBAAJBALSqMkz639g4ym51u169R20b1fqrh03BplKuWpwyOxuMP2m6g1xm\nMmpBx5T8mcWKexVkMQpvN6x1Lg09S4iyAWUCAwEAAQJBAJXu7YHPbjfuoalcUZzF\nbuKRCFtZQRf5z0Os6QvZ8A3iR0SzYJzx+c2ibp7WdifMXp3XaKm4tHSOfumrjUIq\nt10CIQDrs9Xo7bq0zuNjUV5IshNfaiYKZRfQciRVW2O8xBP9VwIhAMQ5CCEDZy+u\nsaF9RtmB0bjbe6XonBlAzoflfH/MAwWjAiEA50hL+ohr0MfCMM7DKaozgEj0kvan\n645VQLywnaX5x3kCIQDCwjinS9FnKmV0e/uOd6PJb0/S5IXLKt/TUpu33K5DMQIh\nAM9peu3B5t9pO59MmeUGZwI+bEJfEb+h03WTptBxS3pO\n-----END RSA PRIVATE KEY-----';
// cp /etc/ssl/openssl.cnf /tmp/openssl.cnf
// echo -e "[SAN]\nsubjectAltName=DNS:amazing.com,DNS:*.amazing.com\n" >> /tmp/openssl.cnf
// openssl req -x509 -newkey rsa:2048 -keyout amazing.key -out amazing.crt -days 3650 -subj /CN=*.amazing.com -nodes -extensions SAN -config /tmp/openssl.cnf
var validCert3 = '-----BEGIN CERTIFICATE-----\nMIIC3DCCAcSgAwIBAgIJALcStAD5sDWEMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV\nBAMMDSouYW1hemluZy5jb20wHhcNMTgwMjA5MjIxMzM2WhcNMjgwMjA3MjIxMzM2\nWjAYMRYwFAYDVQQDDA0qLmFtYXppbmcuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEAvp8dk13u4vmAfKfRNOO8+rVQ8q+vyR8scc9Euj0pTodLBflM\n2K6Zk0isirRzCL/jd4n1A6QrPeJ+r2J4xtHk2j+pavt8Sa2Go2MzpAe3OTuIqYJf\nUt7Im3f2Lb67itTPrpA2TR3A/dDFlazju+eBd3t3496Do8aBPpXAdOabfPsrv3nE\nx97vrr4tzeK3kG9u7GYuod5gyiwF2t5wSeMWbFk2oqkOCtHRXE77JDKVxIGiepnU\nTnkW9b7jIkiBQ1x0xHG4soewV2ymGHS2XrUHZ45FFMG7yVYpytKT9Iz9ty/z5VcL\nZ6NzgU/pKfQaIe8MpoDpVf5UNeB2DOAAEoJKKwIDAQABoykwJzAlBgNVHREEHjAc\nggthbWF6aW5nLmNvbYINKi5hbWF6aW5nLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEA\nMULk6B9XrVPAole8W66o3WUUOrC7NVjbwZjr+Kp5oQTSo84qacaZS2C3ox/j/TZY\nUuNvoE6gIOHi+inN+G4P76K7NEvm8+Y1CeAyaPq01H4Qy2lk9F5wFMtPqvBZnF9C\nx1MvV30FruHXe5pDfnG1npKECpn2SgE3k6FRHM55u8rTMEm/O4TtsDq+fPqUvyWa\nZuRjPv4qVGGkoPyxA6iffxclpOAXs3JUgLcYoM2vxKC0YSOjHEa0p4uffX063Jgg\nybuy3OKvm+8L6moycX7J+LZK81dDTFDtF7PwrnRbpS4re0i/LSk23jDQvDOLnrAa\nSawRR8+1QHTENBo7dnP+NA==\n-----END CERTIFICATE-----';
var validKey3 = '-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC+nx2TXe7i+YB8\np9E047z6tVDyr6/JHyxxz0S6PSlOh0sF+UzYrpmTSKyKtHMIv+N3ifUDpCs94n6v\nYnjG0eTaP6lq+3xJrYajYzOkB7c5O4ipgl9S3sibd/YtvruK1M+ukDZNHcD90MWV\nrOO754F3e3fj3oOjxoE+lcB05pt8+yu/ecTH3u+uvi3N4reQb27sZi6h3mDKLAXa\n3nBJ4xZsWTaiqQ4K0dFcTvskMpXEgaJ6mdROeRb1vuMiSIFDXHTEcbiyh7BXbKYY\ndLZetQdnjkUUwbvJVinK0pP0jP23L/PlVwtno3OBT+kp9Boh7wymgOlV/lQ14HYM\n4AASgkorAgMBAAECggEAdVSVLMcNqlGuv4vAHtDq2lpOaAKxrZbtkWPlxsisqzRl\nfljT7y+RQfHimkG16LXL+iFFWadsIlxOY/+1nZNGTPwQeNQwzVzs2ZbPC3DgW28E\nkGm56NVOHzu4oLGc2DhjWOxVMCRXTSN66sUPK/K0YunxgqXM2zrtBKvCWXI0VLlo\nN/UWAwHf4i0GWRl8u8PvxgMXlSW9p9l6gSsivWRMag9ADwRQ/NSKrRYkiOoRe3vz\nLxXARBvzeZXvOPVLGVRX4SIR7OmS8cC6Ol/rp1/ZFFID7aN+wdzphPSL1UNUriw4\nDv1mxz73SNakgeYSFBoWRS5BsJI01JoCoILsnhVCiQKBgQDyW+k5+j4K17fzwsmi\nyxZ0Nz/ncpkqxVrWYZM3pn7OVkb2NDArimEk53kmJ0hrT84kKJUYDx55R2TpnzpV\nMLmjxgs9TUrzZzsL/DP2ppkfE3OrPS+06OGa5GbURxD6KPvqDtOmU3oFyJ3f4YJR\nVK7RW+zO4sXEpHIxwdBXbYov1QKBgQDJWbt+W5M0sA2D5LrUBNMTvMdNnKH0syc2\nZlcIOdj6HuUIveYpBRq64Jn9VJpXMxQanwE+IUjCpPTa8wF0OA6MZPy6cfovqb8a\ni1/M/lvCoYVS3KHLcTOvTGD3xej0EUj13xWGNu8y3i7Z9/Bl21hEyjd0q0I5OqJx\no9Qa5TGR/wKBgBPfkYpdiMTe14i3ik09FgRFm4nhDcpCEKbPrYC8uF03Ge6KbQDF\nAh5ClN6aDggurRqt8Tvd0YPkZNP7aI8fxbk2PimystiuuFrNPX2WP6warjt2cvkE\nt6s522zAvxWkUrPor1ZONg1PXBLFrSf6J7OnNA3q7oina23FFM52fwRZAoGAZ7l7\nFffU2IKNI9HT0N7/YZ6RSVEUOXuFCsgjs5AhT5BUynERPTZs87I6gb9wltUwWRpq\nSHhbBDJ4FMa0jAtIq1hmvSF0EdOvJ9x+qJqr6JLOnMYd7zDMwFRna5yfigPRgx+9\n9dsc1CaTGiRYyg/5484MTWTgA51KC6Kq5IQHSj8CgYBr9rWgqM8hVCKSt1cMguQV\nTPaV97+u3kV2jFd/aVgDtCDIVvp5TPuqfskE1v3MsSjJ8hfHdYvyxZB8h8T4LlTD\n2HdxwCjVh2qirAvkar2b1mfA6R8msmVaIxBu4MqDcIPqR823klF7A8jSD3MGzYcU\nbnnxMdwgWQkmx0/6/90ZCg==\n-----END PRIVATE KEY-----\n';
it('does not allow empty string for cert', function () {
expect(reverseProxy.validateCertificate('foobar.com', '', 'key')).to.be.an(Error);
});
@@ -108,11 +102,6 @@ describe('Certificates', function () {
it('does not allow invalid cert/key tuple', function () {
expect(reverseProxy.validateCertificate('foobar.com', validCert0, validKey1)).to.be.an(Error);
});
it('picks certificate in SAN', function () {
expect(reverseProxy.validateCertificate('amazing.com', validCert3, validKey3)).to.be(null);
expect(reverseProxy.validateCertificate('subdomain.amazing.com', validCert3, validKey3)).to.be(null);
});
});
describe('getApi - caas', function () {
@@ -144,6 +133,24 @@ describe('Certificates', function () {
done();
});
});
it('returns prod-acme with altDomain in prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain, altDomain: 'foo.something.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
done();
});
});
it('returns prod acme with altDomain in dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain, altDomain: 'foo.something.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
done();
});
});
});
describe('getApi - letsencrypt-prod', function () {
@@ -167,6 +174,15 @@ describe('Certificates', function () {
});
});
it('returns prod acme with altDomain in prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain, altDomain: 'foo.bar.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
done();
});
});
it('returns prod acme in dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
expect(error).to.be(null);
@@ -206,5 +222,14 @@ describe('Certificates', function () {
done();
});
});
it('returns staging acme with altDomain in prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain, altDomain: 'foo.bar.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
done();
});
});
});
});
+3 -13
View File
@@ -7,7 +7,6 @@
var async = require('async'),
config = require('../config.js'),
constants = require('../constants.js'),
database = require('../database.js'),
expect = require('expect.js'),
MockS3 = require('mock-aws-s3'),
@@ -68,16 +67,8 @@ describe('Settings', function () {
});
});
it('can get default app_autoupdate_pattern', function (done) {
settings.getAppAutoupdatePattern(function (error, pattern) {
expect(error).to.be(null);
expect(pattern).to.be('00 30 1,3,5,23 * * *');
done();
});
});
it('can get default box_autoupdate_pattern', function (done) {
settings.getBoxAutoupdatePattern(function (error, pattern) {
it('can get default autoupdate_pattern', function (done) {
settings.getAutoupdatePattern(function (error, pattern) {
expect(error).to.be(null);
expect(pattern).to.be('00 00 1,3,5,23 * * *');
done();
@@ -139,8 +130,7 @@ describe('Settings', function () {
settings.getAll(function (error, allSettings) {
expect(error).to.be(null);
expect(allSettings[settings.TIME_ZONE_KEY]).to.be.a('string');
expect(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]).to.be.a('string');
expect(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]).to.be.a('string');
expect(allSettings[settings.AUTOUPDATE_PATTERN_KEY]).to.be.a('string');
expect(allSettings[settings.CLOUDRON_NAME_KEY]).to.be.a('string');
done();
});
+1 -1
View File
@@ -16,7 +16,7 @@ mkdir -p boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost
mkdir -p platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup
# put cert
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.localhost"))
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes
# create docker network (while the infra code does this, most tests skip infra setup)
docker network create --subnet=172.18.0.0/16 cloudron || true
+2 -4
View File
@@ -78,7 +78,7 @@ describe('updatechecker - box - manual (email)', function () {
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
mail.add.bind(null, DOMAIN_0.domain),
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settings.setBoxAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
settings.setAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' })),
mailer._clearMailQueue
], done);
@@ -296,7 +296,7 @@ describe('updatechecker - app - manual (email)', function () {
mailer._clearMailQueue,
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0),
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settings.setAppAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
settings.setAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
], done);
});
@@ -412,7 +412,6 @@ describe('updatechecker - app - automatic (no email)', function () {
mailer._clearMailQueue,
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0),
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
], done);
});
@@ -478,7 +477,6 @@ describe('updatechecker - app - automatic free (email)', function () {
mailer._clearMailQueue,
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0),
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
], done);
});
+2 -2
View File
@@ -112,7 +112,7 @@ function checkAppUpdates(callback) {
}
// only send notifications if update pattern is 'never'
settings.getAppAutoupdatePattern(function (error, result) {
settings.getAutoupdatePattern(function (error, result) {
if (error) {
debug(error);
} else if (result === constants.AUTOUPDATE_PATTERN_NEVER) {
@@ -168,7 +168,7 @@ function checkBoxUpdates(callback) {
}
// only send notifications if update pattern is 'never'
settings.getBoxAutoupdatePattern(function (error, result) {
settings.getAutoupdatePattern(function (error, result) {
if (error) debug(error);
else if (result === constants.AUTOUPDATE_PATTERN_NEVER) mailer.boxUpdateAvailable(true /* hasSubscription */, updateInfo.version, updateInfo.changelog);
+6 -16
View File
@@ -3,8 +3,6 @@
exports = module.exports = {
UserError: UserError,
removePrivateFields: removePrivateFields,
list: listUsers,
create: createUser,
count: count,
@@ -120,10 +118,6 @@ function validateDisplayName(name) {
return null;
}
function removePrivateFields(user) {
return _.pick(user, 'id', 'username', 'email', 'fallbackEmail', 'displayName', 'groupIds', 'admin');
}
function createUser(username, password, email, displayName, auditSource, options, callback) {
assert(username === null || typeof username === 'string');
assert.strictEqual(typeof password, 'string');
@@ -184,7 +178,7 @@ function createUser(username, password, email, displayName, auditSource, options
callback(null, user);
eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email, user: removePrivateFields(user) });
eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email });
if (!owner) mailer.userAdded(user, sendInvite);
if (sendInvite) mailer.sendInvite(user, invitor);
@@ -198,7 +192,7 @@ function verifyGhost(username, password) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
var ghostData = safe.JSON.parse(safe.fs.readFileSync(constants.GHOST_USER_FILE, 'utf8'));
var ghostData = safe.require(constants.GHOST_USER_FILE);
if (!ghostData) return false;
if (username in ghostData && ghostData[username] === password) {
@@ -275,7 +269,7 @@ function removeUser(userId, auditSource, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_REMOVE, auditSource, { userId: userId, user: removePrivateFields(user) });
eventlog.add(eventlog.ACTION_USER_REMOVE, auditSource, { userId: userId });
callback();
@@ -372,7 +366,7 @@ function updateUser(userId, data, auditSource, callback) {
if (error) return callback(error);
}
userdb.get(userId, function (error, user) {
userdb.get(userId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
@@ -381,13 +375,9 @@ function updateUser(userId, data, auditSource, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, error));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_UPDATE, auditSource, { userId: userId });
callback();
getUser(userId, function (error, result) {
if (error) return console.error(error);
eventlog.add(eventlog.ACTION_USER_UPDATE, auditSource, { userId: userId, user: removePrivateFields(result) });
});
});
});
}