Compare commits

..

70 Commits

Author SHA1 Message Date
Johannes Zellner 99ead48fd5 Add more changes 2018-08-25 11:14:14 +02:00
Johannes Zellner 922b6d2b18 Also renew alternate domain certificates
Part of #583
2018-08-25 11:04:53 +02:00
Girish Ramakrishnan 6a3b45223a Better name for internal functions 2018-08-24 15:38:44 -07:00
Girish Ramakrishnan 93b0f39545 ensureCertificate: make it take appDomain object 2018-08-24 15:07:13 -07:00
Girish Ramakrishnan dbe86af31e Make getApi takes a string domain 2018-08-24 15:00:19 -07:00
Girish Ramakrishnan 3ede50a141 remove unused function 2018-08-24 14:11:02 -07:00
Girish Ramakrishnan e607fe9a41 Specify subdomain fields explicitly 2018-08-24 10:39:59 -07:00
Johannes Zellner 43d125b216 Send hyphenatedSubdomains as non restricted property 2018-08-22 17:19:18 +02:00
Johannes Zellner 9467a2922a Return hyphenated subdomain name in getName() 2018-08-22 14:13:48 +02:00
Johannes Zellner b35c81e546 Handle hyphenatedSubdomains in the backend verifyDnsConfig() 2018-08-22 12:16:19 +02:00
Johannes Zellner 59700e455e Use the full domain record for domains.fqdn() 2018-08-22 12:15:46 +02:00
Johannes Zellner e8fcfc4594 Allow to specify hyphenatedSubdomains for a domain 2018-08-22 11:59:42 +02:00
Johannes Zellner 9bac7e8124 Move the comment where it belongs 2018-08-22 11:02:06 +02:00
Girish Ramakrishnan 210c453508 More changes 2018-08-21 21:25:07 -07:00
Girish Ramakrishnan 442d4e5c6f Fix failing tests 2018-08-21 18:57:11 -07:00
Girish Ramakrishnan af63cb936d More changes 2018-08-21 18:47:12 -07:00
Girish Ramakrishnan b4c9f64721 Issue token on password reset and setup 2018-08-21 18:42:18 -07:00
Girish Ramakrishnan c64a29e6fc More 3.1.0 changes 2018-08-21 17:06:51 -07:00
Johannes Zellner f05df7cfef Allow set admin flag on user creation 2018-08-21 17:12:46 +02:00
Johannes Zellner f4a76a26af Remove left-over docker proxy start call in platform.js 2018-08-21 15:31:53 +02:00
Girish Ramakrishnan f338e015d5 Revert "Also allow docker in containers spawned by an authorized app"
This reverts commit 4f336a05fc.

This is not required by an app yet. Besides, it breaks tests
2018-08-20 20:10:16 -07:00
Girish Ramakrishnan 89cf8167e6 Make tests work 2018-08-20 20:10:14 -07:00
Johannes Zellner d5194cfdc9 Remove nativeLogging docker addon support
Was only required for eclipse che
2018-08-20 15:22:10 +02:00
Johannes Zellner eb07d3d543 Drop all custom network configs in docker proxy 2018-08-20 15:19:08 +02:00
Girish Ramakrishnan 6a1a697820 Split the invite route into two 2018-08-17 16:27:29 -07:00
Girish Ramakrishnan e5cc81d8fa Fix test name 2018-08-17 13:45:10 -07:00
Johannes Zellner 3640b0bd0e Allow to override the logging backend for app like che 2018-08-17 15:30:37 +02:00
Johannes Zellner 4d4ce9b86e Bare bones support of docker exec through the proxy 2018-08-17 15:30:23 +02:00
Johannes Zellner db385c6770 Ensure the docker proxy tests cleanup correctly 2018-08-17 13:44:12 +02:00
Johannes Zellner 2925e98d54 Make PUT requests through the docker proxy work 2018-08-17 12:33:46 +02:00
Girish Ramakrishnan 75ee40865e 3.0.2 changes 2018-08-16 23:30:20 -07:00
Girish Ramakrishnan af58e56732 Fix issue where normal users are shown all apps 2018-08-16 20:04:03 -07:00
Johannes Zellner dc3e3f5f4d Ensure we pipe the parsed body again upstream to docker 2018-08-16 14:28:51 +02:00
Johannes Zellner 83304ff66c Verify that docker logs is correctly reporting another logging driver 2018-08-16 12:07:15 +02:00
Johannes Zellner 575e0cea33 Use syslog for containers created in apps 2018-08-16 11:37:08 +02:00
Girish Ramakrishnan 0bf3b45ddc Fix bind mapping logic 2018-08-15 16:52:30 -07:00
Johannes Zellner 826a0e7708 Add test case for docker logs through the proxy 2018-08-15 18:16:03 +02:00
Johannes Zellner 0522d1e3c4 Simply prefix all docker volume mounts with the app data dir 2018-08-15 18:00:51 +02:00
Johannes Zellner 12970bf50a Add some debugs for volume rewriting 2018-08-15 16:51:10 +02:00
Johannes Zellner 4a739213bf When creating a container the Labels are toplevel 2018-08-15 12:51:52 +02:00
Johannes Zellner 4f336a05fc Also allow docker in containers spawned by an authorized app 2018-08-15 12:35:34 +02:00
Girish Ramakrishnan c3dacba894 dockerproxy: rewrite labels and binds 2018-08-14 20:49:41 -07:00
Girish Ramakrishnan f88c01eea6 dockerproxy: Add app authorization 2018-08-14 20:04:15 -07:00
Girish Ramakrishnan 15b0dfcb60 rename variable 2018-08-14 19:03:59 -07:00
Girish Ramakrishnan ebd27b444d dockerproxy: use express 2018-08-14 18:59:41 -07:00
Johannes Zellner ee1c587922 Overwrite the docker container network in the proxy 2018-08-14 22:54:02 +02:00
Johannes Zellner 4da91ec90d Make the docker proxy work 2018-08-14 22:54:02 +02:00
Girish Ramakrishnan 3cf3c36e86 Fix tests 2018-08-13 22:54:56 -07:00
Girish Ramakrishnan 8bd6c9933f Add interval secs
Part of #568
2018-08-13 22:35:38 -07:00
Girish Ramakrishnan 2e0a7dcd47 Fix migration filename 2018-08-13 22:31:58 -07:00
Johannes Zellner 714c205538 Set the correct debug label 2018-08-13 22:06:28 +02:00
Johannes Zellner 00041add55 No need to pull in underscore to build an object 2018-08-13 22:01:51 +02:00
Johannes Zellner 7f5fe12712 Use DOCKER_HOST to make the cli work out of the box 2018-08-13 21:59:02 +02:00
Johannes Zellner 441fdb81f8 Move docker proxy into its own file 2018-08-13 21:14:17 +02:00
Johannes Zellner fb02e8768c Remove unused require 2018-08-13 21:05:07 +02:00
Johannes Zellner 14f0f954b7 Use docker proxy over DOCKER_URL env var when docker addon is used 2018-08-13 20:47:15 +02:00
Johannes Zellner 10f0d48b2a Use docker proxy port from config 2018-08-13 20:41:02 +02:00
Johannes Zellner 6933184c2e Revert "Expose the host /app/data folder when localstorage and docker addons are used"
We will use the docker proxy for that

This reverts commit b3aa59de19.
2018-08-13 20:39:23 +02:00
Johannes Zellner a1b983de23 Initial code for docker addon proxy 2018-08-13 20:38:39 +02:00
Johannes Zellner b3aa59de19 Expose the host /app/data folder when localstorage and docker addons are used 2018-08-13 19:40:41 +02:00
Girish Ramakrishnan 796ced999f Add some 3.1 changes 2018-08-13 09:18:44 -07:00
Girish Ramakrishnan 353b5e07bf Save correct type of port in db
Part of #504
2018-08-13 08:33:17 -07:00
Girish Ramakrishnan c29eef4c14 Set the udp ports in docker configuration
Part of #504
2018-08-12 22:47:59 -07:00
Girish Ramakrishnan 8bc7dc9724 Pass the manifest to validatePortBindings 2018-08-12 22:37:36 -07:00
Girish Ramakrishnan 60984d18dd Add type field to port bindings table
Part of #504
2018-08-12 22:32:36 -07:00
Girish Ramakrishnan df1dc80fc1 Change the internal portBindings representation
Part of #504
2018-08-12 22:32:31 -07:00
Girish Ramakrishnan 8e2f0cdf73 Update cloudron-manifestformat (udpPorts) 2018-08-12 19:00:28 -07:00
Ian Fijolek bf1e19f8e6 Add more detailed checking of DMARC
Fixes #570
2018-08-12 13:47:24 -07:00
Girish Ramakrishnan 9a7214ea07 Update manifestformat for docker addon 2018-08-10 12:31:46 -07:00
Johannes Zellner 4499f08357 Revert "Remove _docker addon"
This reverts commit d6f49eb54f.
2018-08-09 11:54:46 +02:00
49 changed files with 747 additions and 290 deletions
+9 -1
View File
@@ -1351,5 +1351,13 @@
[3.0.2]
* Fix issue where normal users are shown apps they don't have access to
* Re-configure mail apps when mail is enabled/disabled
* Re-configure email apps when email is enabled/disabled
[3.1.0]
* Add UDP support
* Clicking invite button does not send an invite immediately
* Implement docker addon
* Automatically login after password reset and account setup
* Make backup interval configurable
* Fix alternate domain certificate renewal
+7
View File
@@ -13,6 +13,7 @@ var appHealthMonitor = require('./src/apphealthmonitor.js'),
async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
dockerProxy = require('./src/dockerproxy.js'),
server = require('./src/server.js');
console.log();
@@ -25,6 +26,9 @@ console.log(' Version: ', config.version());
console.log(' Admin Origin: ', config.adminOrigin());
console.log(' Appstore API server origin: ', config.apiServerOrigin());
console.log(' Appstore Web server origin: ', config.webServerOrigin());
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
console.log(' LDAP Server Port: ', config.get('ldapPort'));
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
console.log();
console.log('==========================================');
console.log();
@@ -32,6 +36,7 @@ console.log();
async.series([
server.start,
ldap.start,
dockerProxy.start,
appHealthMonitor.start,
], function (error) {
if (error) {
@@ -46,11 +51,13 @@ var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () {
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
@@ -0,0 +1,18 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE appPortBindings ADD COLUMN type VARCHAR(8) NOT NULL DEFAULT "tcp"'),
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP INDEX hostPort'), // this drops the unique constraint
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort, type)')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort)'),
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP COLUMN type')
], callback);
};
@@ -0,0 +1,16 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
backupConfig.intervalSecs = 24 * 60 * 60;
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
+1
View File
@@ -92,6 +92,7 @@ CREATE TABLE IF NOT EXISTS apps(
CREATE TABLE IF NOT EXISTS appPortBindings(
hostPort INTEGER NOT NULL UNIQUE,
type VARCHAR(8) NOT NULL DEFAULT "tcp",
environmentVariable VARCHAR(128) NOT NULL,
appId VARCHAR(128) NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id),
+14 -64
View File
@@ -1632,15 +1632,15 @@
}
},
"cloudron-manifestformat": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.11.0.tgz",
"integrity": "sha512-t4KR2KmK1JDtxw1n6IVNg0+xxspk/Cpb6m1WimE9hJ0KJYsIgZNnkce47uYiG9/nWrgUSV4xcdzsS91OOvWgig==",
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.13.1.tgz",
"integrity": "sha512-KvWaUw0q2U+EL+y7LJ+Q8YZfERYgyGRwj48ZRfsREaIjckS8mG03Oa2UIf2x/uGLfw0frWvTNdQXe3ZpC94N9g==",
"requires": {
"cron": "1.3.0",
"java-packagename-regex": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"java-packagename-regex": "1.0.0",
"safetydance": "0.0.15",
"semver": "4.3.6",
"tv4": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
"tv4": "1.3.0",
"validator": "3.43.0"
},
"dependencies": {
@@ -2649,6 +2649,7 @@
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.6.tgz",
"integrity": "sha1-QooiOv4DQl0s1tY0f99AxmkDVj0=",
"optional": true,
"requires": {
"nan": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz"
}
@@ -3178,32 +3179,6 @@
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"fs-extra": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.6.4.tgz",
"integrity": "sha1-9G8MdbeEH40gCzNIzU1pHVoJnRU=",
"dev": true,
"requires": {
"jsonfile": "1.0.1",
"mkdirp": "0.3.5",
"ncp": "0.4.2",
"rimraf": "2.2.8"
},
"dependencies": {
"mkdirp": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=",
"dev": true
},
"rimraf": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=",
"dev": true
}
}
},
"fs.realpath": {
"version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
@@ -4490,7 +4465,8 @@
}
},
"java-packagename-regex": {
"version": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"integrity": "sha1-lR9he9WhlCIO0GcLm4KowOxcYiQ="
},
"js-base64": {
@@ -4549,12 +4525,6 @@
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"dev": true
},
"jsonfile": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
"integrity": "sha1-6l7+QLg2kLmGZ2FKc5L8YOhCwN0=",
"dev": true
},
"jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
@@ -5158,22 +5128,6 @@
}
}
},
"mock-aws-s3": {
"version": "git+https://github.com/cloudron-io/mock-aws-s3.git#1306f1722b82897382a2339d52a94ded15003d8c",
"dev": true,
"requires": {
"fs-extra": "0.6.4",
"underscore": "1.8.3"
},
"dependencies": {
"underscore": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=",
"dev": true
}
}
},
"modelo": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.3.tgz",
@@ -5401,12 +5355,6 @@
"integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=",
"optional": true
},
"ncp": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz",
"integrity": "sha1-q8xsvT7C7Spyn/bnwfqPAXhKhXQ=",
"dev": true
},
"negotiator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
@@ -6813,7 +6761,7 @@
"readdirp": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
"integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=",
"integrity": "sha512-LgQ8mdp6hbxJUZz27qxVl7gmFM/0DfHRO52c5RUbKAgMvr81tour7YYWW1JYNmrXyD/o0Myy9/DC3fUYkqnyzg==",
"requires": {
"graceful-fs": "4.1.11",
"minimatch": "3.0.4",
@@ -7310,7 +7258,7 @@
"rimraf": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
"integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=",
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
"requires": {
"glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz"
}
@@ -7327,7 +7275,8 @@
"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=="
"integrity": "sha512-EzBtUaFH9bHYPc69wqjp0efJI/DPNHdFbGE3uIMn4sVbO0zx8vZ8cG4WKxQfOpUOKsQyGBiT2mTqnCw+6nLswA==",
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -8479,7 +8428,8 @@
"dev": true
},
"tv4": {
"version": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
"integrity": "sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM="
},
"tweetnacl": {
+2 -1
View File
@@ -20,7 +20,8 @@
"async": "^2.6.1",
"aws-sdk": "^2.253.1",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.11.0",
"cloudron-manifestformat": "^2.13.1",
"connect": "^3.6.6",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
"connect-timeout": "^1.9.0",
+1 -1
View File
@@ -90,8 +90,8 @@ server {
add_header Referrer-Policy "no-referrer-when-downgrade";
proxy_hide_header Referrer-Policy;
# CSP headers for the admin/dashboard resources
<% if ( endpoint === 'admin' ) { -%>
# CSP headers for the admin/dashboard resources
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
<% } -%>
+8
View File
@@ -109,6 +109,12 @@ var KNOWN_ADDONS = {
teardown: NOOP,
backup: NOOP,
restore: NOOP
},
docker: {
setup: NOOP,
teardown: NOOP,
backup: NOOP,
restore: NOOP
}
};
@@ -199,6 +205,8 @@ function getEnvironment(app, callback) {
appdb.getAddonConfigByAppId(app.id, function (error, result) {
if (error) return callback(error);
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${config.get('dockerProxyPort')}` });
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
});
}
+28 -23
View File
@@ -71,7 +71,9 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.ts' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
const SUBDOMAIN_FIELDS = [ 'appId', 'domain', 'subdomain', 'type' ].join(',');
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
@@ -96,14 +98,16 @@ function postProcess(result) {
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
result.portBindings = { };
var hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
var environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(',');
delete result.hostPorts;
delete result.environmentVariables;
delete result.portTypes;
for (var i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
}
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
@@ -133,15 +137,15 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
result[0].alternateDomains = alternateDomains;
@@ -158,15 +162,15 @@ function getByHttpPort(httpPort, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
result[0].alternateDomains = alternateDomains;
@@ -182,15 +186,15 @@ function getByContainerId(containerId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
result[0].alternateDomains = alternateDomains;
@@ -205,14 +209,14 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('SELECT * FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
alternateDomains.forEach(function (d) {
@@ -271,8 +275,8 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
Object.keys(portBindings).forEach(function (env) {
queries.push({
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, appId) VALUES (?, ?, ?)',
args: [ env, portBindings[env], id ]
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)',
args: [ env, portBindings[env].hostPort, portBindings[env].type, id ]
});
});
@@ -322,18 +326,19 @@ function getPortBindings(id, callback) {
var portBindings = { };
for (var i = 0; i < results.length; i++) {
portBindings[results[i].environmentVariable] = results[i].hostPort;
portBindings[results[i].environmentVariable] = { hostPort: results[i].hostPort, type: results[i].type };
}
callback(null, portBindings);
});
}
function delPortBinding(hostPort, callback) {
function delPortBinding(hostPort, type, callback) {
assert.strictEqual(typeof hostPort, 'number');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appPortBindings WHERE hostPort=?', [ hostPort ], function (error, result) {
database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -394,8 +399,8 @@ function updateWithConstraints(id, app, constraints, callback) {
// replace entries by app id
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
Object.keys(portBindings).forEach(function (env) {
var values = [ portBindings[env], env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, environmentVariable, appId) VALUES(?, ?, ?)', args: values });
var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values });
});
}
+67 -30
View File
@@ -45,10 +45,14 @@ exports = module.exports = {
setOwner: setOwner,
transferOwnership: transferOwnership,
PORT_TYPE_TCP: 'tcp',
PORT_TYPE_UDP: 'udp',
// exported for testing
_validateHostname: validateHostname,
_validatePortBindings: validatePortBindings,
_validateAccessRestriction: validateAccessRestriction
_validateAccessRestriction: validateAccessRestriction,
_translatePortBindings: translatePortBindings
};
var appdb = require('./appdb.js'),
@@ -158,8 +162,9 @@ function validateHostname(location, domain, hostname) {
}
// validate the port bindings
function validatePortBindings(portBindings, tcpPorts) {
function validatePortBindings(portBindings, manifest) {
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof manifest, 'object');
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
@@ -190,26 +195,51 @@ function validatePortBindings(portBindings, tcpPorts) {
if (!portBindings) return null;
var env;
for (env in portBindings) {
if (!/^[a-zA-Z0-9_]+$/.test(env)) return new AppsError(AppsError.BAD_FIELD, env + ' is not valid environment variable');
for (let portName in portBindings) {
if (!/^[a-zA-Z0-9_]+$/.test(portName)) return new AppsError(AppsError.BAD_FIELD, `${portName} is not a valid environment variable`);
if (!Number.isInteger(portBindings[env])) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not an integer');
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
if (portBindings[env] <= 1023 || portBindings[env] > 65535) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not in permitted range');
const hostPort = portBindings[portName];
if (!Number.isInteger(hostPort)) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not an integer`);
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(hostPort));
if (hostPort <= 1023 || hostPort > 65535) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not in permitted range`);
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
// that the user wants the service disabled
tcpPorts = tcpPorts || { };
for (env in portBindings) {
if (!(env in tcpPorts)) return new AppsError(AppsError.BAD_FIELD, 'Invalid portBindings ' + env);
const tcpPorts = manifest.tcpPorts || { };
const udpPorts = manifest.udpPorts || { };
for (let portName in portBindings) {
if (!(portName in tcpPorts) && !(portName in udpPorts)) return new AppsError(AppsError.BAD_FIELD, `Invalid portBindings ${portName}`);
}
return null;
}
function translatePortBindings(portBindings, manifest) {
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof manifest, 'object');
if (!portBindings) return null;
let result = {};
const tcpPorts = manifest.tcpPorts || { };
for (let portName in portBindings) {
const portType = portName in tcpPorts ? exports.PORT_TYPE_TCP : exports.PORT_TYPE_UDP;
result[portName] = { hostPort: portBindings[portName], type: portType };
}
return result;
}
function postProcess(app) {
let result = {};
for (let portName in app.portBindings) {
result[portName] = app.portBindings[portName].hostPort;
}
app.portBindings = result;
}
function validateAccessRestriction(accessRestriction) {
assert.strictEqual(typeof accessRestriction, 'object');
@@ -305,8 +335,8 @@ function getDuplicateErrorDetails(location, portBindings, error) {
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
// check if any of the port bindings conflict
for (var env in portBindings) {
if (portBindings[env] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
for (let portName in portBindings) {
if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
}
return new AppsError(AppsError.ALREADY_EXISTS);
@@ -375,11 +405,13 @@ function get(appId, 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));
domaindb.get(app.domain, function (error, result) {
postProcess(app);
domaindb.get(app.domain, function (error, domainObject) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = domains.fqdn(app.location, app.domain, domainObject);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -403,11 +435,13 @@ function getByIpAddress(ip, 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));
domaindb.get(app.domain, function (error, result) {
postProcess(app);
domaindb.get(app.domain, function (error, domainObject) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = domains.fqdn(app.location, app.domain, domainObject);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -427,12 +461,14 @@ function getAll(callback) {
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
apps.forEach(postProcess);
async.eachSeries(apps, function (app, iteratorDone) {
domaindb.get(app.domain, function (error, result) {
domaindb.get(app.domain, function (error, domainObject) {
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = domains.fqdn(app.location, app.domain, domainObject);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -520,7 +556,7 @@ function install(data, auditSource, callback) {
error = checkManifestConstraints(manifest);
if (error) return callback(error);
error = validatePortBindings(portBindings, manifest.tcpPorts);
error = validatePortBindings(portBindings, manifest);
if (error) return callback(error);
error = validateAccessRestriction(accessRestriction);
@@ -559,7 +595,7 @@ function install(data, auditSource, callback) {
if (error && error.reason === DomainsError.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 fqdn = domains.fqdn(location, domain, domainObject);
error = validateHostname(location, domain, fqdn);
if (error) return callback(error);
@@ -583,7 +619,7 @@ function install(data, auditSource, callback) {
robotsTxt: robotsTxt
};
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, portBindings, data, function (error) {
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), 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));
@@ -649,9 +685,10 @@ function configure(appId, data, auditSource, callback) {
}
if ('portBindings' in data) {
portBindings = values.portBindings = data.portBindings;
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
error = validatePortBindings(data.portBindings, app.manifest);
if (error) return callback(error);
values.portBindings = translatePortBindings(data.portBindings, app.manifest);
portBindings = data.portBindings;
} else {
portBindings = app.portBindings;
}
@@ -694,7 +731,7 @@ function configure(appId, data, auditSource, callback) {
if (error && error.reason === DomainsError.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 fqdn = domains.fqdn(location, domain, domainObject);
error = validateHostname(location, domain, fqdn);
if (error) return callback(error);
@@ -950,14 +987,14 @@ function clone(appId, data, auditSource, callback) {
error = checkManifestConstraints(backupInfo.manifest);
if (error) return callback(error);
error = validatePortBindings(portBindings, backupInfo.manifest.tcpPorts);
error = validatePortBindings(portBindings, backupInfo.manifest);
if (error) return callback(error);
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.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));
error = validateHostname(location, domain, domains.fqdn(location, domain, domainObject));
if (error) return callback(error);
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
@@ -974,7 +1011,7 @@ function clone(appId, data, auditSource, callback) {
robotsTxt: app.robotsTxt
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, portBindings, data, function (error) {
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -1162,8 +1199,8 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
var newTcpPorts = newManifest.tcpPorts || { };
var portBindings = app.portBindings; // this is never null
for (var env in portBindings) {
if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it');
for (let portName in portBindings) {
if (!(portName in newTcpPorts)) return new Error(`${portName} was in use but new update removes it`);
}
// it's fine if one or more (unused) keys got removed
+5 -5
View File
@@ -670,14 +670,14 @@ function update(app, callback) {
// free unused ports
function (next) {
// make sure we always have objects
var currentPorts = app.portBindings || {};
var newPorts = app.updateConfig.manifest.tcpPorts || {};
const currentPorts = app.portBindings || {};
const newTcpPorts = app.updateConfig.manifest.tcpPorts || {};
const newUdpPorts = app.updateConfig.manifest.udpPorts || {};
async.each(Object.keys(currentPorts), function (portName, callback) {
if (newPorts[portName]) return callback(); // port still in use
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(); // port still in use
appdb.delPortBinding(currentPorts[portName], function (error) {
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.');
else if (error) return next(error);
+11 -5
View File
@@ -124,6 +124,8 @@ function testConfig(backupConfig, callback) {
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BackupsError(BackupsError.BAD_FIELD, 'unknown format'));
if (backupConfig.intervalSecs < 6 * 60 * 60) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Interval must be atleast 6 hours'));
api(backupConfig.provider).testConfig(backupConfig, callback);
}
@@ -984,12 +986,16 @@ function ensureBackup(auditSource, callback) {
return callback(error); // no point trying to backup if appstore is down
}
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
debug('Previous backup was %j, no need to backup now', backups[0]);
return callback(null);
}
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
backup(auditSource, callback);
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < (backupConfig.intervalSecs - 3600) * 1000)) { // adjust 1 hour
debug('Previous backup was %j, no need to backup now', backups[0]);
return callback(null);
}
backup(auditSource, callback);
});
});
}
+1
View File
@@ -103,6 +103,7 @@ function initConfig() {
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.dockerProxyPort = 3003;
// keep in sync with start.sh
data.database = {
-2
View File
@@ -19,8 +19,6 @@ exports = module.exports = {
ADMIN_NAME: 'Settings',
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
+2 -1
View File
@@ -130,7 +130,8 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
var credentials = {
token: dnsConfig.token,
fqdn: domain
fqdn: domain,
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
};
const testSubdomain = 'cloudrontestdns';
+2 -1
View File
@@ -234,7 +234,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
var credentials = {
token: dnsConfig.token,
email: dnsConfig.email
email: dnsConfig.email,
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
+2 -1
View File
@@ -203,7 +203,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
var credentials = {
token: dnsConfig.token
token: dnsConfig.token,
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
+2 -1
View File
@@ -115,7 +115,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
var credentials = {
token: dnsConfig.token
token: dnsConfig.token,
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
+2 -1
View File
@@ -24,7 +24,8 @@ function getDnsCredentials(dnsConfig) {
credentials: {
client_email: dnsConfig.credentials.client_email,
private_key: dnsConfig.credentials.private_key
}
},
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
}
+2 -1
View File
@@ -151,7 +151,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
var credentials = {
apiKey: dnsConfig.apiKey,
apiSecret: dnsConfig.apiSecret
apiSecret: dnsConfig.apiSecret,
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
+6 -1
View File
@@ -55,11 +55,16 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var config = {
wildcard: !!dnsConfig.wildcard,
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
}
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
callback(null, { wildcard: !!dnsConfig.wildcard });
callback(null, config);
});
}
+2 -1
View File
@@ -210,7 +210,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
var credentials = {
username: dnsConfig.username,
token: dnsConfig.token
token: dnsConfig.token,
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
+2 -1
View File
@@ -241,7 +241,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
secretAccessKey: dnsConfig.secretAccessKey,
region: dnsConfig.region || 'us-east-1',
endpoint: dnsConfig.endpoint || null,
listHostedZonesByName: true // new/updated creds require this perm
listHostedZonesByName: true, // new/updated creds require this perm
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
+9 -6
View File
@@ -127,14 +127,17 @@ function createSubcontainer(app, name, cmd, options, callback) {
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
var portEnv = [];
for (var e in app.portBindings) {
var hostPort = app.portBindings[e];
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
for (let portName in app.portBindings) {
const hostPort = app.portBindings[portName];
const portType = portName in manifest.tcpPorts ? 'tcp' : 'udp';
const ports = portType == 'tcp' ? manifest.tcpPorts : manifest.udpPorts;
exposedPorts[containerPort + '/tcp'] = {};
portEnv.push(e + '=' + hostPort);
var containerPort = ports[portName].containerPort || hostPort;
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
exposedPorts[`${containerPort}/${portType}`] = {};
portEnv.push(`${portName}=${hostPort}`);
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
}
// first check db record, then manifest
+178
View File
@@ -0,0 +1,178 @@
'use strict';
exports = module.exports = {
start: start,
stop: stop
};
var apps = require('./apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
config = require('./config.js'),
express = require('express'),
debug = require('debug')('box:dockerproxy'),
http = require('http'),
HttpError = require('connect-lastmile').HttpError,
middleware = require('./middleware'),
net = require('net'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
_ = require('underscore');
var gHttpServer = null;
function authorizeApp(req, res, next) {
// TODO add here some authorization
// - block apps not using the docker addon
// - block calls regarding platform containers
// - only allow managing and inspection of containers belonging to the app
// make the tests pass for now
if (config.TEST) {
req.app = { id: 'testappid' };
return next();
}
apps.getByIpAddress(req.connection.remoteAddress, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
if (error) return next(new HttpError(500, error));
if (!('docker' in app.manifest.addons)) return next(new HttpError(401, 'Unauthorized'));
req.app = app;
next();
});
}
function attachDockerRequest(req, res, next) {
var options = {
socketPath: '/var/run/docker.sock',
method: req.method,
path: req.url,
headers: req.headers
};
req.dockerRequest = http.request(options, function (dockerResponse) {
res.writeHead(dockerResponse.statusCode, dockerResponse.headers);
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
res.write(' ');
dockerResponse.on('error', function (error) { console.error('dockerResponse error:', error); });
dockerResponse.pipe(res, { end: true });
});
next();
}
function containersCreate(req, res, next) {
safe.set(req.body, 'HostConfig.NetworkMode', 'cloudron'); // overwrite the network the container lives in
safe.set(req.body, 'NetworkingConfig', {}); // drop any custom network configs
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id })); // overwrite the app id to track containers of an app
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
dockerDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'docker');
debug('Original volume binds:', req.body.HostConfig.Binds);
let binds = [];
for (let bind of (req.body.HostConfig.Binds || [])) {
if (bind.startsWith(appDataDir)) binds.push(bind); // eclipse will inspect docker to find out the host folders and pass that to child containers
else if (bind.startsWith('/app/data')) binds.push(bind.replace(new RegExp('^/app/data'), appDataDir));
else binds.push(`${dockerDataDir}/${bind}`);
}
// cleanup the paths from potential double slashes
binds = binds.map(function (bind) { return bind.replace(/\/+/g, '/'); });
debug('Rewritten volume binds:', binds);
safe.set(req.body, 'HostConfig.Binds', binds);
let plainBody = JSON.stringify(req.body);
req.dockerRequest.setHeader('Content-Length', Buffer.byteLength(plainBody));
req.dockerRequest.end(plainBody);
}
function process(req, res, next) {
// we have to rebuild the body since we consumed in in the parser
if (Object.keys(req.body).length !== 0) {
let plainBody = JSON.stringify(req.body);
req.dockerRequest.setHeader('Content-Length', Buffer.byteLength(plainBody));
req.dockerRequest.end(plainBody);
} else if (!req.readable) {
req.dockerRequest.end();
} else {
req.pipe(req.dockerRequest, { end: true });
}
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
assert(gHttpServer === null, 'Already started');
let json = middleware.json({ strict: true });
let router = new express.Router();
router.post('/:version/containers/create', containersCreate);
let proxyServer = express();
if (config.TEST) {
proxyServer.use(function (req, res, next) {
console.log('Proxying: ' + req.method, req.url);
next();
});
}
proxyServer.use(authorizeApp)
.use(attachDockerRequest)
.use(json)
.use(router)
.use(process)
.use(middleware.lastMile());
gHttpServer = http.createServer(proxyServer);
gHttpServer.listen(config.get('dockerProxyPort'), '0.0.0.0', callback);
debug(`startDockerProxy: started proxy on port ${config.get('dockerProxyPort')}`);
gHttpServer.on('upgrade', function (req, client, head) {
// Create a new tcp connection to the TCP server
var remote = net.connect('/var/run/docker.sock', function () {
var upgradeMessage = req.method + ' ' + req.url + ' HTTP/1.1\r\n' +
`Host: ${req.headers.host}\r\n` +
'Connection: Upgrade\r\n' +
'Upgrade: tcp\r\n';
if (req.headers['content-type'] === 'application/json') {
// TODO we have to parse the immediate upgrade request body, but I don't know how
let plainBody = '{"Detach":false,"Tty":false}\r\n';
upgradeMessage += `Content-Type: application/json\r\n`;
upgradeMessage += `Content-Length: ${Buffer.byteLength(plainBody)}\r\n`;
upgradeMessage += '\r\n';
upgradeMessage += plainBody;
}
upgradeMessage += '\r\n';
// resend the upgrade event to the docker daemon, so it responds with the proper message through the pipes
remote.write(upgradeMessage);
// two-way pipes between client and docker daemon
client.pipe(remote).pipe(client);
});
});
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
if (gHttpServer) gHttpServer.close();
gHttpServer = null;
callback();
}
+8 -5
View File
@@ -104,7 +104,6 @@ function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
api(provider).verifyDnsConfig(config, domain, zoneName, ip, callback);
}
function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
@@ -269,7 +268,7 @@ function getName(domain, subdomain) {
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
return subdomain === '' ? part : subdomain + '.' + part;
return subdomain === '' ? part : (subdomain + (domain.config.hyphenatedSubdomains ? '-' : '.') + part);
}
function getDnsRecords(subdomain, domain, type, callback) {
@@ -360,7 +359,7 @@ function setAdmin(domain, callback) {
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.provider === 'caas' ? '-' : '.') + result.domain);
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
callback();
@@ -369,8 +368,8 @@ function setAdmin(domain, callback) {
});
}
function fqdn(location, domain, provider) {
return location + (location ? (provider !== 'caas' ? '.' : '-') : '') + domain;
function fqdn(location, domain, domainObject) {
return location + (location ? (domainObject.config.hyphenatedSubdomains ? '-' : '.') : '') + domain;
}
// removes all fields that are strictly private and should never be returned by API calls
@@ -383,5 +382,9 @@ function removePrivateFields(domain) {
// removes all fields that are not accessible by a normal user
function removeRestrictedFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider');
// always ensure config object
result.config = { hyphenatedSubdomains: !!domain.config.hyphenatedSubdomains };
return result;
}
-1
View File
@@ -10,7 +10,6 @@ var assert = require('assert'),
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:ldap'),
eventlog = require('./eventlog.js'),
+12 -1
View File
@@ -278,6 +278,15 @@ function checkMx(domain, callback) {
});
}
function txtToDict(txt) {
var dict = {};
txt.split(';').forEach(function(v) {
var p = v.trim().split('=');
dict[p[0]]=p[1];
});
return dict;
}
function checkDmarc(domain, callback) {
var dmarc = {
domain: '_dmarc.' + domain,
@@ -293,7 +302,9 @@ function checkDmarc(domain, callback) {
if (txtRecords.length !== 0) {
dmarc.value = txtRecords[0].join('');
dmarc.status = (dmarc.value === dmarc.expected);
// allow extra fields in dmarc like rua
const actual = txtToDict(dmarc.value), expected = txtToDict(dmarc.expected);
dmarc.status = Object.keys(expected).every(k => expected[k] === actual[k]);
}
callback(null, dmarc);
+3 -4
View File
@@ -226,11 +226,10 @@ function sendInvite(user, invitor) {
});
}
function userAdded(user, inviteSent) {
function userAdded(user) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof inviteSent, 'boolean');
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
debug('Sending mail for userAdded');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
@@ -239,7 +238,7 @@ function userAdded(user, inviteSent) {
var templateData = {
user: user,
inviteLink: inviteSent ? null : `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
inviteLink: `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
+50 -38
View File
@@ -77,22 +77,22 @@ ReverseProxyError.INTERNAL_ERROR = 'Internal Error';
ReverseProxyError.INVALID_CERT = 'Invalid certificate';
ReverseProxyError.NOT_FOUND = 'Not Found';
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
function getApi(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
domains.get(app.domain, function (error, domain) {
domains.get(domain, function (error, result) {
if (error) return callback(error);
if (domain.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
if (result.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
var api = domain.tlsConfig.provider === 'caas' ? caas : acme;
var api = result.tlsConfig.provider === 'caas' ? caas : acme;
var options = { };
if (domain.tlsConfig.provider === 'caas') {
if (result.tlsConfig.provider === 'caas') {
options.prod = true;
} else { // acme
options.prod = domain.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
options.prod = result.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
}
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
@@ -127,14 +127,6 @@ function validateCertificate(domain, cert, key) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
function matchesDomain(candidate) {
if (typeof candidate !== 'string') return false;
if (candidate === domain) return true;
if (candidate.indexOf('*') === 0 && candidate.slice(2) === domain.slice(domain.indexOf('.') + 1)) return true;
return false;
}
// check for empty cert and key strings
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
@@ -229,12 +221,14 @@ function getCertificate(app, callback) {
return getFallbackCertificate(app.domain, callback);
}
function ensureCertificate(app, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
function ensureCertificate(appDomain, auditSource, callback) {
assert.strictEqual(typeof appDomain, 'object');
assert.strictEqual(typeof appDomain.fqdn, 'string');
assert.strictEqual(typeof appDomain.domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const vhost = app.fqdn;
const vhost = appDomain.fqdn;
var certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
@@ -256,7 +250,7 @@ function ensureCertificate(app, auditSource, callback) {
debug('ensureCertificate: %s cert does not exist', vhost);
}
getApi(app, function (error, api, apiOptions) {
getApi(appDomain.domain, function (error, api, apiOptions) {
if (error) return callback(error);
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
@@ -272,14 +266,14 @@ 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(appDomain.domain, callback);
callback(null, { certFilePath, keyFilePath, reason: 'new-le' });
});
});
}
function configureAdminInternal(bundle, configFileName, vhost, callback) {
function writeAdminConfig(bundle, configFileName, vhost, callback) {
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
@@ -308,15 +302,15 @@ function configureAdmin(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var adminApp = { domain: config.adminDomain(), fqdn: config.adminFqdn() };
ensureCertificate(adminApp, auditSource, function (error, bundle) {
var adminAppDomain = { domain: config.adminDomain(), fqdn: config.adminFqdn() };
ensureCertificate(adminAppDomain, auditSource, function (error, bundle) {
if (error) return callback(error);
configureAdminInternal(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
writeAdminConfig(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
});
}
function configureAppInternal(app, bundle, callback) {
function writeAppConfig(app, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -349,7 +343,7 @@ function configureAppInternal(app, bundle, callback) {
reload(callback);
}
function configureAppRedirect(app, fqdn, bundle, callback) {
function writeAppRedirectConfig(app, fqdn, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof bundle, 'object');
@@ -388,7 +382,7 @@ function configureApp(app, auditSource, callback) {
ensureCertificate({ fqdn: app.fqdn, domain: app.domain }, auditSource, function (error, bundle) {
if (error) return callback(error);
configureAppInternal(app, bundle, function (error) {
writeAppConfig(app, bundle, function (error) {
if (error) return callback(error);
// now setup alternateDomain redirects if any
@@ -398,7 +392,7 @@ function configureApp(app, auditSource, callback) {
ensureCertificate({ fqdn: fqdn, domain: domain.domain }, auditSource, function (error, bundle) {
if (error) return callback(error);
configureAppRedirect(app, fqdn, bundle, callback);
writeAppRedirectConfig(app, fqdn, bundle, callback);
});
}, callback);
});
@@ -426,24 +420,42 @@ 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
var allDomains = [];
async.eachSeries(allApps, function (app, iteratorCallback) {
ensureCertificate(app, auditSource, function (error, bundle) {
// add webadmin domain
allDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin' });
// add app main
allApps.forEach(function (app) {
allDomains.push({ domain: app.domain, fqdn: app.fqdn, type: 'main', app: app });
// and alternate domains
app.alternateDomains.forEach(function (domain) {
// TODO support hyphenated domains here as well
var fqdn = (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain;
allDomains.push({ domain: domain.domain, fqdn: fqdn, type: 'alternate', app: app });
});
});
async.eachSeries(allDomains, function (domain, iteratorCallback) {
ensureCertificate(domain, auditSource, function (error, bundle) {
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
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() ?
configureAdminInternal.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: configureAppInternal.bind(null, app, bundle);
var configureFunc;
if (domain.type === 'webadmin') configureFunc = writeAdminConfig.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn());
else if (domain.type === 'main') configureFunc = writeAppConfig.bind(null, domain.app, bundle);
else if (domain.type === 'alternate') configureFunc = writeAppRedirectConfig.bind(null, domain.app, domain.fqdn, bundle);
else return callback(new Error(`Unknown domain type for ${domain.fqdn}. This should never happen`));
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
if (ignoredError) debug('renewAll: error reconfiguring app', ignoredError);
platform.handleCertChanged(app.fqdn);
platform.handleCertChanged(domain.fqdn);
iteratorCallback(); // move to next app
iteratorCallback(); // move to next domain
});
});
});
@@ -470,7 +482,7 @@ function configureDefaultServer(callback) {
safe.child_process.execSync(certCommand);
}
configureAdminInternal({ certFilePath, keyFilePath }, 'default.conf', '', function (error) {
writeAdminConfig({ certFilePath, keyFilePath }, 'default.conf', '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
+10 -3
View File
@@ -362,10 +362,13 @@ function accountSetup(req, res, next) {
// setPassword clears the resetToken
users.setPassword(userObject.id, req.body.password, function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error) return next(new HttpError(500, error));
res.redirect(config.adminOrigin());
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
if (error) return next(new HttpError(500, error));
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
});
});
});
});
@@ -409,7 +412,11 @@ function passwordReset(req, res, next) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(406, error.message));
if (error) return next(new HttpError(500, error));
res.redirect(config.adminOrigin());
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
if (error) return next(new HttpError(500, error));
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
});
});
});
}
+1
View File
@@ -158,6 +158,7 @@ function setBackupConfig(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
if (typeof req.body.intervalSecs !== 'number') return next(new HttpError(400, 'intervalSecs is required'));
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('syncConcurrency' in req.body) {
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
+1 -1
View File
@@ -141,7 +141,7 @@ describe('Eventlog API', function () {
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(1);
expect(result.body.eventlogs.length).to.equal(2);
done();
});
+19
View File
@@ -408,6 +408,25 @@ describe('Mail API', function () {
});
});
it('succeeds with modified DMARC1 values', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC1; p=reject; rua=mailto:rua@example.com; pct=100']];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
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.status).to.eql(true);
expect(res.body.dns.dmarc.value).to.eql('v=DMARC1; p=reject; rua=mailto:rua@example.com; pct=100');
done();
});
});
it('succeeds with all correct records', function (done) {
clearDnsAnswerQueue();
+3 -1
View File
@@ -1481,9 +1481,11 @@ describe('Password', function () {
it('succeeds', function (done) {
var scope = nock(config.adminOrigin())
.filteringPath(function (path) {
path = path.replace(/accessToken=[^&]*/, 'accessToken=token');
path = path.replace(/expiresAt=[^&]*/, 'expiresAt=1234');
return path;
})
.get('/').reply(200, {});
.get('/?accessToken=token&expiresAt=1234').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: '12345678', email: USER_0.email, resetToken: USER_0.resetToken })
+41 -37
View File
@@ -266,11 +266,9 @@ describe('Users API', function () {
});
it('cannot create user without email', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, invite: true })
.send({ username: USERNAME_1 })
.end(function (error, result) {
expect(error).to.be.ok();
expect(result.statusCode).to.equal(400);
@@ -279,46 +277,63 @@ describe('Users API', function () {
});
it('create second user succeeds', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, email: EMAIL_1, invite: true })
.send({ username: USERNAME_1, email: EMAIL_1 })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
user_1 = result.body;
checkMails(2, function () {
// 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() + 10000, accesscontrol.SCOPE_PROFILE, done);
});
// 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() + 10000, accesscontrol.SCOPE_PROFILE, done);
});
});
it('reinvite unknown user fails', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1+USERNAME_1 + '/invite')
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1+USERNAME_1 + '/create_invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.be.an(Error);
expect(res.statusCode).to.equal(404);
checkMails(0, done);
done();
});
});
it('reinvite second user succeeds', function (done) {
mailer._clearMailQueue();
it('send invite without creating invite fails succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/send_invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.be.an(Error);
expect(res.statusCode).to.equal(409);
done();
});
});
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/invite')
it('create invite second user succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/create_invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.not.be.ok();
expect(res.statusCode).to.equal(200);
expect(res.body.resetToken).to.be.ok();
done();
});
});
it('can send invite', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/send_invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.be(null);
expect(res.statusCode).to.equal(200);
checkMails(1, done);
});
});
@@ -384,12 +399,12 @@ describe('Users API', function () {
});
});
it('create user missing username fails', function (done) {
it('create user missing username succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ email: EMAIL_2 })
.send({ email: `unnamed${EMAIL_2}` })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(result.statusCode).to.equal(201);
done();
});
});
@@ -404,16 +419,6 @@ describe('Users API', function () {
});
});
it('create user missing invite fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL_2 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('create user reserved name fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
@@ -436,10 +441,9 @@ describe('Users API', function () {
it('create second and third user', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL_2, invite: false })
.send({ username: USERNAME_2, email: EMAIL_2 })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
@@ -447,12 +451,12 @@ describe('Users API', function () {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_3, email: EMAIL_3, invite: true })
.send({ username: USERNAME_3, email: EMAIL_3 })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
// one mail for first user creation, two mails for second user creation (see 'invite' flag)
checkMails(3, done);
// two mails for user creation
checkMails(2, done);
});
});
});
@@ -496,13 +500,13 @@ describe('Users API', function () {
expect(error).to.be(null);
expect(res.statusCode).to.equal(200);
expect(res.body.users).to.be.an('array');
expect(res.body.users.length).to.equal(4);
expect(res.body.users.length).to.equal(5);
res.body.users.forEach(function (user) {
expect(user).to.be.an('object');
expect(user.id).to.be.ok();
expect(user.username).to.be.ok();
expect(user.email).to.be.ok();
if (!user.email.startsWith('unnamed')) expect(user.username).to.be.ok();
expect(user.password).to.not.be.ok();
expect(user.salt).to.not.be.ok();
expect(user.groupIds).to.not.be.ok();
+18 -6
View File
@@ -7,6 +7,7 @@ exports = module.exports = {
create: create,
remove: remove,
verifyPassword: verifyPassword,
createInvite: createInvite,
sendInvite: sendInvite,
setGroups: setGroups,
transferOwnership: transferOwnership
@@ -27,18 +28,17 @@ function create(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if (typeof req.body.invite !== 'boolean') return next(new HttpError(400, 'invite must be boolean'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if ('admin' in req.body && typeof req.body.admin !== 'boolean') return next(new HttpError(400, 'admin flag must be a boolean'));
var password = req.body.password || null;
var email = req.body.email;
var sendInvite = req.body.invite;
var username = 'username' in req.body ? req.body.username : null;
var displayName = req.body.displayName || '';
users.create(username, password, email, displayName, { invitor: req.user, sendInvite: sendInvite }, auditSource(req), function (error, user) {
users.create(username, password, email, displayName, { invitor: req.user, admin: req.body.admin }, auditSource(req), function (error, user) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
@@ -137,14 +137,26 @@ function verifyPassword(req, res, next) {
});
}
function sendInvite(req, res, next) {
function createInvite(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
users.sendInvite(req.params.userId, { invitor: req.user }, function (error, result) {
users.createInvite(req.params.userId, function (error, resetToken) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { resetToken: result }));
next(new HttpSuccess(200, { resetToken: resetToken }));
});
}
function sendInvite(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
users.sendInvite(req.params.userId, { invitor: req.user }, function (error) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(409, 'Call createInvite API first'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { }));
});
}
+2 -1
View File
@@ -150,7 +150,8 @@ function initializeExpressSync() {
router.del ('/api/v1/users/:userId', usersManageScope, routes.users.verifyPassword, routes.users.remove);
router.post('/api/v1/users/:userId', usersManageScope, routes.users.update);
router.put ('/api/v1/users/:userId/groups', usersManageScope, routes.users.setGroups);
router.post('/api/v1/users/:userId/invite', usersManageScope, routes.users.sendInvite);
router.post('/api/v1/users/:userId/send_invite', usersManageScope, routes.users.sendInvite);
router.post('/api/v1/users/:userId/create_invite', usersManageScope, routes.users.createInvite);
router.post('/api/v1/users/:userId/transfer', usersManageScope, routes.users.transferOwnership);
// Group management
+2 -1
View File
@@ -90,7 +90,8 @@ var gDefaults = (function () {
provider: 'filesystem',
key: '',
backupFolder: '/var/backups',
retentionSecs: 172800
retentionSecs: 2 * 24 * 60 * 60, // 2 days
intervalSecs: 24 * 60 * 60 // ~1 day
};
result[exports.UPDATE_CONFIG_KEY] = { prerelease: false };
result[exports.APPSTORE_CONFIG_KEY] = {};
+12 -12
View File
@@ -162,9 +162,9 @@ describe('Apps', function () {
groupdb.add.bind(null, GROUP_0.id, GROUP_0.name),
groupdb.add.bind(null, GROUP_1.id, GROUP_1.name),
groups.addMember.bind(null, GROUP_0.id, USER_1.id),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, APP_1.ownerId, APP_1.portBindings, APP_1),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, APP_2.ownerId, APP_2.portBindings, APP_2),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, APP_1.ownerId, apps._translatePortBindings(APP_1.portBindings, APP_1.manifest), APP_1),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, APP_2.ownerId, apps._translatePortBindings(APP_2.portBindings, APP_2.manifest), APP_2),
settingsdb.set.bind(null, settings.BACKUP_CONFIG_KEY, JSON.stringify({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }))
], done);
});
@@ -216,26 +216,26 @@ describe('Apps', function () {
describe('validatePortBindings', function () {
it('does not allow invalid host port', function () {
expect(apps._validatePortBindings({ port: -1 }, { port: 5000 })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 0 }, { port: 5000 })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 'text' }, { port: 5000 })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 65536 }, { port: 5000 })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 470 }, { port: 5000 })).to.be.an(Error);
expect(apps._validatePortBindings({ port: -1 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 0 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 'text' }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 65536 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 470 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
});
it('does not allow ports not as part of manifest', function () {
expect(apps._validatePortBindings({ port: 1567 }, { })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 1567 }, { port3: null })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 1567 }, { tcpPorts: { } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 1567 }, { tcpPorts: { port3: null } })).to.be.an(Error);
});
it('allows valid bindings', function () {
expect(apps._validatePortBindings({ port: 1024 }, { port: 5000 })).to.be(null);
expect(apps._validatePortBindings({ port: 1024 }, { tcpPorts: { port: 5000 } })).to.be(null);
expect(apps._validatePortBindings({
port1: 4033,
port2: 3242,
port3: 1234
}, { port1: null, port2: null, port3: null })).to.be(null);
}, { tcpPorts: { port1: null, port2: null, port3: null } })).to.be(null);
});
});
+3 -3
View File
@@ -221,7 +221,7 @@ describe('database', function () {
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
httpPort: null,
containerId: null,
portBindings: { port: 5678 },
portBindings: { port: { hostPort: 5678, type: 'tcp' } },
health: null,
accessRestriction: null,
lastBackupId: null,
@@ -735,7 +735,7 @@ describe('database', function () {
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
httpPort: null,
containerId: null,
portBindings: { port: 5678 },
portBindings: { port: { hostPort: 5678, type: 'tcp' } },
health: null,
accessRestriction: null,
restoreConfig: null,
@@ -821,7 +821,7 @@ describe('database', function () {
appdb.getPortBindings(APP_0.id, function (error, bindings) {
expect(error).to.be(null);
expect(bindings).to.be.an(Object);
expect(bindings).to.be.eql({ port: '5678' });
expect(bindings).to.be.eql({ port: { hostPort: '5678', type: 'tcp' } });
done();
});
});
+106
View File
@@ -0,0 +1,106 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var dockerProxy = require('../dockerproxy.js'),
config = require('../config.js'),
exec = require('child_process').exec,
expect = require('expect.js');
const DOCKER = `docker -H tcp://localhost:${config.get('dockerProxyPort')} `;
describe('Dockerproxy', function () {
var containerId;
// create a container to test against
before(function (done) {
dockerProxy.start(function (error) {
expect(error).to.not.be.ok();
exec(`${DOCKER} run -d ubuntu "bin/bash" "-c" "while true; do echo 'perpetual walrus'; sleep 1; done"`, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stderr).to.be.empty();
containerId = stdout.slice(0, -1); // removes the trailing \n
done();
});
});
});
after(function (done) {
exec(`${DOCKER} rm -f ${containerId}`, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stderr).to.be.empty();
dockerProxy.stop(done);
});
});
// uncomment this to run the proxy for manual testing
// this.timeout(1000000);
// it('wait', function (done) {} );
it('can get info', function (done) {
exec(DOCKER + ' info', function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stdout).to.contain('Containers:');
// expect(stderr).to.be.empty(); // on some machines, i get 'No swap limit support'
done();
});
});
it('can create container', function (done) {
var cmd = DOCKER + ` run ubuntu "/bin/bash" "-c" "echo 'hello'"`;
exec(cmd, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stdout).to.contain('hello');
expect(stderr).to.be.empty();
done();
});
});
it('proxy overwrites the container network option', function (done) {
var cmd = `${DOCKER} run --network ifnotrewritethiswouldfail ubuntu "/bin/bash" "-c" "echo 'hello'"`;
exec(cmd, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stdout).to.contain('hello');
expect(stderr).to.be.empty();
done();
});
});
it('cannot see logs through docker logs, since syslog is configured', function (done) {
exec(`${DOCKER} logs ${containerId}`, function (error, stdout, stderr) {
expect(error.message).to.contain('configured logging driver does not support reading');
expect(stderr).to.contain('configured logging driver does not support reading');
expect(stdout).to.be.empty();
done();
});
});
it('can use PUT to upload archive into a container', function (done) {
exec(`${DOCKER} cp -a ${__dirname}/proxytestarchive.tar ${containerId}:/tmp/`, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stderr).to.be.empty();
expect(stdout).to.be.empty();
done();
});
});
it('can exec into a container', function (done) {
exec(`${DOCKER} exec ${containerId} ls`, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stderr).to.be.empty();
expect(stdout).to.equal('bin\nboot\ndev\netc\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n');
done();
});
});
});
+2 -1
View File
@@ -7,6 +7,7 @@
'use strict';
var appdb = require('../appdb.js'),
apps = require('../apps.js'),
assert = require('assert'),
async = require('async'),
database = require('../database.js'),
@@ -105,7 +106,7 @@ function setup(done) {
USER_0.id = APP_0.ownerId = result.id;
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0, callback);
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, callback);
});
},
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
Binary file not shown.
+6 -6
View File
@@ -128,7 +128,7 @@ describe('Certificates', function () {
after(cleanup);
it('returns prod caas for prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
reverseProxy._getApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('caas');
expect(options.prod).to.be(true);
@@ -137,7 +137,7 @@ describe('Certificates', function () {
});
it('returns prod caas for dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
reverseProxy._getApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('caas');
expect(options.prod).to.be(true);
@@ -159,7 +159,7 @@ describe('Certificates', function () {
after(cleanup);
it('returns prod acme in prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
reverseProxy._getApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -168,7 +168,7 @@ describe('Certificates', function () {
});
it('returns prod acme in dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
reverseProxy._getApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -190,7 +190,7 @@ describe('Certificates', function () {
after(cleanup);
it('returns staging acme in prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
reverseProxy._getApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
@@ -199,7 +199,7 @@ describe('Certificates', function () {
});
it('returns staging acme in dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
reverseProxy._getApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
+4 -3
View File
@@ -6,6 +6,7 @@
'use strict';
var appdb = require('../appdb.js'),
apps = require('../apps.js'),
async = require('async'),
config = require('../config.js'),
constants = require('../constants.js'),
@@ -300,7 +301,7 @@ describe('updatechecker - app - manual (email)', function () {
if (error) return next(error);
APP_0.ownerId = userObject.id;
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0, next);
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, next);
});
},
settings.setAppAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
@@ -423,7 +424,7 @@ describe('updatechecker - app - automatic (no email)', function () {
if (error) return next(error);
APP_0.ownerId = userObject.id;
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0, next);
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, next);
});
},
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
@@ -496,7 +497,7 @@ describe('updatechecker - app - automatic free (email)', function () {
if (error) return next(error);
APP_0.ownerId = userObject.id;
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0, next);
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, next);
});
},
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
+21 -7
View File
@@ -176,7 +176,7 @@ describe('User', function () {
});
});
it('succeeds and attempts to send invite', function (done) {
it('succeeds', function (done) {
users.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).not.to.be.ok();
expect(result).to.be.ok();
@@ -184,8 +184,7 @@ describe('User', function () {
expect(result.email).to.equal(EMAIL.toLowerCase());
expect(result.fallbackEmail).to.equal(EMAIL.toLowerCase());
// first user is owner, do not send mail to admins
checkMails(0, done);
done();
});
});
@@ -222,7 +221,7 @@ describe('User', function () {
expect(result.fallbackEmail).to.equal(EMAIL_1.toLowerCase());
// first user is owner, do not send mail to admins
checkMails(2, { sentTo: EMAIL_1.toLowerCase() }, function (error) {
checkMails(1, { sentTo: EMAIL_1.toLowerCase() }, function (error) {
expect(error).not.to.be.ok();
maildb.update(DOMAIN_0.domain, { enabled: false }, done);
@@ -830,7 +829,7 @@ describe('User', function () {
});
});
describe('send invite', function () {
describe('invite', function () {
before(createOwner);
after(cleanupUsers);
@@ -843,9 +842,24 @@ describe('User', function () {
});
});
it('succeeds', function (done) {
it('fails as expected', function (done) {
users.sendInvite(userObject.id, { }, function (error) {
expect(error).to.eql(null);
expect(error).to.be.ok(); // have to create resetToken first
done();
});
});
it('can create token', function (done) {
users.createInvite(userObject.id, function (error, resetToken) {
expect(error).to.be(null);
expect(resetToken).to.be.ok();
done();
});
});
it('send invite', function (done) {
users.sendInvite(userObject.id, { }, function (error) {
expect(error).to.be(null);
checkMails(1, done);
});
});
+26 -12
View File
@@ -21,6 +21,7 @@ exports = module.exports = {
update: updateUser,
createOwner: createOwner,
getOwner: getOwner,
createInvite: createInvite,
sendInvite: sendInvite,
setMembership: setMembership,
setTwoFactorAuthenticationSecret: setTwoFactorAuthenticationSecret,
@@ -150,9 +151,9 @@ function create(username, password, email, displayName, options, auditSource, ca
assert(options && typeof options === 'object');
assert.strictEqual(typeof auditSource, 'object');
var invitor = options.invitor || null,
sendInvite = !!options.sendInvite,
owner = !!options.owner;
const isOwner = !!options.owner;
const isAdmin = !!options.admin;
const invitor = options.invitor || null;
var error;
@@ -192,9 +193,9 @@ function create(username, password, email, displayName, options, auditSource, ca
salt: salt.toString('hex'),
createdAt: now,
modifiedAt: now,
resetToken: hat(256),
resetToken: '',
displayName: displayName,
admin: owner
admin: isOwner || isAdmin
};
userdb.add(user.id, user, function (error) {
@@ -203,10 +204,9 @@ function create(username, password, email, displayName, options, auditSource, ca
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, user: removePrivateFields(user), invitor: invitor });
if (!owner) mailer.userAdded(user, sendInvite);
if (sendInvite) mailer.sendInvite(user, invitor);
if (!isOwner) mailer.userAdded(user);
});
});
});
@@ -523,9 +523,8 @@ function getOwner(callback) {
});
}
function sendInvite(userId, options, callback) {
function createInvite(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
userdb.get(userId, function (error, userObject) {
@@ -538,13 +537,28 @@ function sendInvite(userId, options, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UsersError(UsersError.NOT_FOUND));
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
mailer.sendInvite(userObject, options.invitor || null);
callback(null, userObject.resetToken);
});
});
}
function sendInvite(userId, options, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
userdb.get(userId, function (error, userObject) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UsersError(UsersError.NOT_FOUND));
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
if (!userObject.resetToken) return callback(new UsersError(UsersError.BAD_FIELD, 'Must generate resetToken to send inivitation'));
mailer.sendInvite(userObject, options.invitor || null);
callback(null);
});
}
function setTwoFactorAuthenticationSecret(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');