Compare commits
171 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9854598648 | |||
| 1e7e2e5e97 | |||
| 081e496878 | |||
| aaff7f463a | |||
| 55f937bf51 | |||
| d5d1d061bb | |||
| bc6f602891 | |||
| ca461057e7 | |||
| b1c5c2468a | |||
| 562ce3192f | |||
| 3787dd98b4 | |||
| 6c667e4325 | |||
| 0eec693a85 | |||
| c3bf672c2a | |||
| c3a3b6412f | |||
| 44291b842a | |||
| 36cf502b56 | |||
| 2df77cf280 | |||
| a453e49c27 | |||
| e34c34de46 | |||
| 8dc5bf96e3 | |||
| d2c3e1d1ae | |||
| 4eab101b78 | |||
| e460d6d15b | |||
| 3012f68a56 | |||
| 1909050be2 | |||
| d4c62c7295 | |||
| 4eb3d1b918 | |||
| fb6bf50e48 | |||
| d8213f99b1 | |||
| 7d7b759930 | |||
| 6f2bc555e0 | |||
| a8c43ddf4a | |||
| 3eabc27877 | |||
| ad379bd766 | |||
| c1047535d4 | |||
| 10142cc00b | |||
| 5e1487d12a | |||
| 39e0c13701 | |||
| c80d984ee6 | |||
| 3e474767d1 | |||
| e2b954439c | |||
| 950d1eb5c3 | |||
| f48a2520c3 | |||
| f366d41034 | |||
| b3c40e1ba7 | |||
| b686d6e011 | |||
| 5663198bfb | |||
| bad50fd78b | |||
| 9bd43e3f74 | |||
| f0fefab8ad | |||
| 449231c791 | |||
| bd161ec677 | |||
| 8040d4ac2d | |||
| 06d7820566 | |||
| 4818a3feee | |||
| 9fcb2c0733 | |||
| 6906b4159a | |||
| 763b9309f6 | |||
| 2bb4d1c22b | |||
| 23303363ee | |||
| 79c17abad2 | |||
| 3234e0e3f0 | |||
| 982cd1e1f3 | |||
| df39fc86a4 | |||
| 870e0c4144 | |||
| 57704b706e | |||
| 223e0dfd1f | |||
| 51c438b0b6 | |||
| 93d210a754 | |||
| 265ee15ac7 | |||
| d0da47e0b3 | |||
| 0e8553d1a7 | |||
| 9229dd2fd5 | |||
| c2a8744240 | |||
| bc7e07f6a6 | |||
| bfd6f8965e | |||
| eb1e4a1aea | |||
| dc3e8a9cb5 | |||
| 494bcc1711 | |||
| 7e071d9f23 | |||
| 6f821222db | |||
| 6e464dbc81 | |||
| be8ef370c6 | |||
| 39a05665b0 | |||
| 737e22116a | |||
| 43e1e4829f | |||
| c95778178f | |||
| 04870313b7 | |||
| 6ca040149c | |||
| e487b9d46b | |||
| 1375e16ad2 | |||
| 312f1f0085 | |||
| 721900fc47 | |||
| 2d815a92a3 | |||
| 1c192b7c11 | |||
| 4a887336bc | |||
| 8f6521f942 | |||
| fbdfaa4dc7 | |||
| bf4290db3e | |||
| 94ad633128 | |||
| c552917991 | |||
| a7ee8c853e | |||
| 29e4879451 | |||
| 8b92344808 | |||
| 0877cec2e6 | |||
| b1ca577be7 | |||
| 9b484f5ac9 | |||
| b6a9fd81da | |||
| f19113f88e | |||
| 3837bee51f | |||
| 89c3296632 | |||
| db55f0696e | |||
| 03d4ae9058 | |||
| f8b41b703c | |||
| 2a989e455c | |||
| cd24decca0 | |||
| f39842a001 | |||
| 2a39526a4c | |||
| ded5d4c98b | |||
| a0ca59c3f2 | |||
| 53cfc49807 | |||
| 942eb579e4 | |||
| 5819cfe412 | |||
| 5cb62ca412 | |||
| df10c245de | |||
| 4a804dc52b | |||
| ed2f25a998 | |||
| 7510c9fe29 | |||
| 78a1d53728 | |||
| e9b078cd58 | |||
| dd8b928684 | |||
| 185b574bdc | |||
| a89726a8c6 | |||
| c80aca27e6 | |||
| 029acab333 | |||
| 4f9f10e130 | |||
| 9ba11d2e14 | |||
| 23a5a1f79f | |||
| e8dc617d40 | |||
| d56794e846 | |||
| 2663ec7da0 | |||
| eec4ae98cd | |||
| c31a0f4e09 | |||
| 739db23514 | |||
| 8598fb444b | |||
| 0b630ff504 | |||
| 84169dea3d | |||
| d83b5de47a | |||
| 2719c4240f | |||
| d749756b53 | |||
| 0401c61c15 | |||
| 34f45da2de | |||
| baecbf783c | |||
| 2f141cd6e0 | |||
| 1296299d02 | |||
| 998ac74d32 | |||
| b4a34e6432 | |||
| e70c9d55db | |||
| 268aee6265 | |||
| 1ba7b0e0fb | |||
| 72788fdb11 | |||
| 435afec13c | |||
| 2cb1877669 | |||
| edd672cba7 | |||
| 991f37fe05 | |||
| c147d8004b | |||
| cdcc4dfda8 | |||
| 2eaba686fb | |||
| 236032b4a6 | |||
| 5fcba59b3e |
@@ -4,10 +4,17 @@
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs() {
|
||||
arguments[0] = this.namespace + ' ' + arguments[0];
|
||||
return arguments;
|
||||
};
|
||||
|
||||
var appHealthMonitor = require('./src/apphealthmonitor.js'),
|
||||
async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
simpleauth = require('./src/simpleauth.js'),
|
||||
oauthproxy = require('./src/oauthproxy.js'),
|
||||
server = require('./src/server.js');
|
||||
|
||||
@@ -29,8 +36,9 @@ console.log();
|
||||
async.series([
|
||||
server.start,
|
||||
ldap.start,
|
||||
simpleauth.start,
|
||||
appHealthMonitor.start,
|
||||
oauthproxy.start.bind(null, 4000 /* port */)
|
||||
oauthproxy.start
|
||||
], function (error) {
|
||||
if (error) {
|
||||
console.error('Error starting server', error);
|
||||
@@ -43,6 +51,7 @@ var NOOP_CALLBACK = function () { };
|
||||
process.on('SIGINT', function () {
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
simpleauth.stop(NOOP_CALLBACK);
|
||||
oauthproxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
@@ -50,6 +59,7 @@ process.on('SIGINT', function () {
|
||||
process.on('SIGTERM', function () {
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
simpleauth.stop(NOOP_CALLBACK);
|
||||
oauthproxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs() {
|
||||
arguments[0] = this.namespace + ' ' + arguments[0];
|
||||
return arguments;
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:janitor'),
|
||||
async = require('async'),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN oauthProxy BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN oauthProxy', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ CREATE TABLE IF NOT EXISTS tokens(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clients(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
appId VARCHAR(128) NOT NULL, // this is for the form <type>-appId to allow easy clearing of tokens of a type
|
||||
clientSecret VARCHAR(512) NOT NULL,
|
||||
redirectURI VARCHAR(512) NOT NULL,
|
||||
scope VARCHAR(512) NOT NULL,
|
||||
@@ -49,10 +49,14 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
location VARCHAR(128) NOT NULL UNIQUE,
|
||||
dnsRecordId VARCHAR(512),
|
||||
accessRestriction VARCHAR(512),
|
||||
oauthProxy BOOLEAN DEFAULT 0,
|
||||
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
lastBackupId VARCHAR(128),
|
||||
lastBackupConfigJson VARCHAR(2048), // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
|
||||
|
||||
oldConfigJson VARCHAR(2048), // used to pass old config for apptask
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appPortBindings(
|
||||
|
||||
Generated
+3496
-2274
File diff suppressed because it is too large
Load Diff
+1
-4
@@ -12,14 +12,11 @@
|
||||
"engines": [
|
||||
"node >= 0.12.0"
|
||||
],
|
||||
"bin": {
|
||||
"cloudron": "./app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^1.2.1",
|
||||
"aws-sdk": "^2.1.46",
|
||||
"body-parser": "^1.13.1",
|
||||
"cloudron-manifestformat": "^1.7.0",
|
||||
"cloudron-manifestformat": "^1.9.1",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "0.0.13",
|
||||
"connect-timeout": "^1.5.0",
|
||||
|
||||
+8
-8
@@ -3,15 +3,15 @@
|
||||
# If you change the infra version, be sure to put a warning
|
||||
# in the change log
|
||||
|
||||
INFRA_VERSION=8
|
||||
INFRA_VERSION=16
|
||||
|
||||
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
|
||||
# These constants are used in the installer script as well
|
||||
BASE_IMAGE=cloudron/base:0.3.3
|
||||
MYSQL_IMAGE=cloudron/mysql:0.3.3
|
||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.3.2
|
||||
MONGODB_IMAGE=cloudron/mongodb:0.3.2
|
||||
REDIS_IMAGE=cloudron/redis:0.3.2 # if you change this, fix src/addons.js as well
|
||||
MAIL_IMAGE=cloudron/mail:0.3.2
|
||||
GRAPHITE_IMAGE=cloudron/graphite:0.3.4
|
||||
BASE_IMAGE=cloudron/base:0.6.0
|
||||
MYSQL_IMAGE=cloudron/mysql:0.6.0
|
||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.6.0
|
||||
MONGODB_IMAGE=cloudron/mongodb:0.6.0
|
||||
REDIS_IMAGE=cloudron/redis:0.6.1 # if you change this, fix src/addons.js as well
|
||||
MAIL_IMAGE=cloudron/mail:0.6.0
|
||||
GRAPHITE_IMAGE=cloudron/graphite:0.6.0
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ cp "${container_files}/sudoers" /etc/sudoers.d/yellowtent
|
||||
rm -rf /etc/collectd
|
||||
ln -sfF "${DATA_DIR}/collectd" /etc/collectd
|
||||
|
||||
########## apparmor docker profile
|
||||
cp "${container_files}/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
systemctl restart apparmor
|
||||
|
||||
########## nginx
|
||||
# link nginx config to system config
|
||||
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#include <tunables/global>
|
||||
|
||||
|
||||
profile docker-cloudron-app flags=(attach_disconnected,mediate_deleted) {
|
||||
|
||||
#include <abstractions/base>
|
||||
|
||||
ptrace peer=@{profile_name},
|
||||
|
||||
network,
|
||||
capability,
|
||||
file,
|
||||
umount,
|
||||
|
||||
deny @{PROC}/sys/fs/** wklx,
|
||||
deny @{PROC}/sysrq-trigger rwklx,
|
||||
deny @{PROC}/mem rwklx,
|
||||
deny @{PROC}/kmem rwklx,
|
||||
deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx,
|
||||
deny @{PROC}/sys/kernel/*/** wklx,
|
||||
|
||||
deny mount,
|
||||
|
||||
deny /sys/[^f]*/** wklx,
|
||||
deny /sys/f[^s]*/** wklx,
|
||||
deny /sys/fs/[^c]*/** wklx,
|
||||
deny /sys/fs/c[^g]*/** wklx,
|
||||
deny /sys/fs/cg[^r]*/** wklx,
|
||||
deny /sys/firmware/efi/efivars/** rwklx,
|
||||
deny /sys/kernel/security/** rwklx,
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ server {
|
||||
}
|
||||
|
||||
<% } else if ( endpoint === 'oauthproxy' ) { %>
|
||||
proxy_pass http://127.0.0.1:4000;
|
||||
proxy_pass http://127.0.0.1:3003;
|
||||
proxy_set_header X-Cloudron-Proxy-Port <%= port %>;
|
||||
<% } else if ( endpoint === 'app' ) { %>
|
||||
proxy_pass http://127.0.0.1:<%= port %>;
|
||||
|
||||
@@ -28,19 +28,24 @@ fi
|
||||
|
||||
# graphite
|
||||
graphite_container_id=$(docker run --restart=always -d --name="graphite" \
|
||||
-m 75m \
|
||||
--memory-swap 150m \
|
||||
-p 127.0.0.1:2003:2003 \
|
||||
-p 127.0.0.1:2004:2004 \
|
||||
-p 127.0.0.1:8000:8000 \
|
||||
-v "${DATA_DIR}/graphite:/app/data" \
|
||||
--read-only -v /tmp -v /run -v /var/log \
|
||||
"${GRAPHITE_IMAGE}")
|
||||
echo "Graphite container id: ${graphite_container_id}"
|
||||
|
||||
# mail
|
||||
# mail (MAIL_SMTP_PORT is 2500 in addons.js. used in mailer.js as well)
|
||||
mail_container_id=$(docker run --restart=always -d --name="mail" \
|
||||
-p 127.0.0.1:25:25 \
|
||||
-m 75m \
|
||||
--memory-swap 150m \
|
||||
-h "${arg_fqdn}" \
|
||||
-e "DOMAIN_NAME=${arg_fqdn}" \
|
||||
-v "${DATA_DIR}/box/mail:/app/data" \
|
||||
--read-only -v /tmp -v /run -v /var/log \
|
||||
"${MAIL_IMAGE}")
|
||||
echo "Mail container id: ${mail_container_id}"
|
||||
|
||||
@@ -52,9 +57,12 @@ readonly MYSQL_ROOT_PASSWORD='${mysql_addon_root_password}'
|
||||
readonly MYSQL_ROOT_HOST='${docker0_ip}'
|
||||
EOF
|
||||
mysql_container_id=$(docker run --restart=always -d --name="mysql" \
|
||||
-m 100m \
|
||||
--memory-swap 200m \
|
||||
-h "${arg_fqdn}" \
|
||||
-v "${DATA_DIR}/mysql:/var/lib/mysql" \
|
||||
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
|
||||
--read-only -v /tmp -v /run -v /var/log \
|
||||
"${MYSQL_IMAGE}")
|
||||
echo "MySQL container id: ${mysql_container_id}"
|
||||
|
||||
@@ -64,9 +72,12 @@ cat > "${DATA_DIR}/addons/postgresql_vars.sh" <<EOF
|
||||
readonly POSTGRESQL_ROOT_PASSWORD='${postgresql_addon_root_password}'
|
||||
EOF
|
||||
postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
|
||||
-m 100m \
|
||||
--memory-swap 200m \
|
||||
-h "${arg_fqdn}" \
|
||||
-v "${DATA_DIR}/postgresql:/var/lib/postgresql" \
|
||||
-v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
|
||||
--read-only -v /tmp -v /run -v /var/log \
|
||||
"${POSTGRESQL_IMAGE}")
|
||||
echo "PostgreSQL container id: ${postgresql_container_id}"
|
||||
|
||||
@@ -76,19 +87,23 @@ cat > "${DATA_DIR}/addons/mongodb_vars.sh" <<EOF
|
||||
readonly MONGODB_ROOT_PASSWORD='${mongodb_addon_root_password}'
|
||||
EOF
|
||||
mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
|
||||
-m 100m \
|
||||
--memory-swap 200m \
|
||||
-h "${arg_fqdn}" \
|
||||
-v "${DATA_DIR}/mongodb:/var/lib/mongodb" \
|
||||
-v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
|
||||
--read-only -v /tmp -v /run -v /var/log \
|
||||
"${MONGODB_IMAGE}")
|
||||
echo "Mongodb container id: ${mongodb_container_id}"
|
||||
|
||||
# only touch apps in installed state. any other state is just resumed by the taskmanager
|
||||
if [[ "${infra_version}" == "none" ]]; then
|
||||
# if no existing infra was found (for new and restoring cloudons), download app backups
|
||||
# if no existing infra was found (for new, upgraded and restored cloudons), download app backups
|
||||
echo "Marking installed apps for restore"
|
||||
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_restore" WHERE installationState = "installed"' box
|
||||
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_restore", oldConfigJson = NULL WHERE installationState = "installed"' box
|
||||
else
|
||||
# if existing infra was found, just mark apps for reconfiguration
|
||||
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_configure" WHERE installationState = "installed"' box
|
||||
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_configure", oldConfigJson = NULL WHERE installationState = "installed"' box
|
||||
fi
|
||||
|
||||
echo -n "${INFRA_VERSION}" > "${DATA_DIR}/INFRA_VERSION"
|
||||
|
||||
+169
-88
@@ -11,8 +11,8 @@ exports = module.exports = {
|
||||
getBindsSync: getBindsSync,
|
||||
|
||||
// exported for testing
|
||||
_allocateOAuthCredentials: allocateOAuthCredentials,
|
||||
_removeOAuthCredentials: removeOAuthCredentials
|
||||
_setupOauth: setupOauth,
|
||||
_teardownOauth: teardownOauth
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
@@ -35,27 +35,26 @@ var appdb = require('./appdb.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = child_process.spawn,
|
||||
tokendb = require('./tokendb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid'),
|
||||
vbox = require('./vbox.js');
|
||||
|
||||
var NOOP = function (app, callback) { return callback(); };
|
||||
var NOOP = function (app, options, callback) { return callback(); };
|
||||
|
||||
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
|
||||
// teardown is destructive. app data stored with the addon is lost
|
||||
var KNOWN_ADDONS = {
|
||||
oauth: {
|
||||
setup: allocateOAuthCredentials,
|
||||
teardown: removeOAuthCredentials,
|
||||
setup: setupOauth,
|
||||
teardown: teardownOauth,
|
||||
backup: NOOP,
|
||||
restore: allocateOAuthCredentials
|
||||
restore: setupOauth
|
||||
},
|
||||
token: {
|
||||
setup: allocateAccessToken,
|
||||
teardown: removeAccessToken,
|
||||
simpleauth: {
|
||||
setup: setupSimpleAuth,
|
||||
teardown: teardownSimpleAuth,
|
||||
backup: NOOP,
|
||||
restore: allocateAccessToken
|
||||
restore: setupSimpleAuth
|
||||
},
|
||||
ldap: {
|
||||
setup: setupLdap,
|
||||
@@ -90,7 +89,7 @@ var KNOWN_ADDONS = {
|
||||
redis: {
|
||||
setup: setupRedis,
|
||||
teardown: teardownRedis,
|
||||
backup: NOOP, // no backup because we store redis as part of app's volume
|
||||
backup: backupRedis,
|
||||
restore: setupRedis // same thing
|
||||
},
|
||||
localstorage: {
|
||||
@@ -128,9 +127,9 @@ function setupAddons(app, addons, callback) {
|
||||
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
|
||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
|
||||
|
||||
debugApp(app, 'Setting up addon %s', addon);
|
||||
debugApp(app, 'Setting up addon %s with options %j', addon, addons[addon]);
|
||||
|
||||
KNOWN_ADDONS[addon].setup(app, iteratorCallback);
|
||||
KNOWN_ADDONS[addon].setup(app, addons[addon], iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -146,9 +145,9 @@ function teardownAddons(app, addons, callback) {
|
||||
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
|
||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
|
||||
|
||||
debugApp(app, 'Tearing down addon %s', addon);
|
||||
debugApp(app, 'Tearing down addon %s with options %j', addon, addons[addon]);
|
||||
|
||||
KNOWN_ADDONS[addon].teardown(app, iteratorCallback);
|
||||
KNOWN_ADDONS[addon].teardown(app, addons[addon], iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -166,7 +165,7 @@ function backupAddons(app, addons, callback) {
|
||||
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
|
||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
|
||||
|
||||
KNOWN_ADDONS[addon].backup(app, iteratorCallback);
|
||||
KNOWN_ADDONS[addon].backup(app, addons[addon], iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -184,7 +183,7 @@ function restoreAddons(app, addons, callback) {
|
||||
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
|
||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
|
||||
|
||||
KNOWN_ADDONS[addon].restore(app, iteratorCallback);
|
||||
KNOWN_ADDONS[addon].restore(app, addons[addon], iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -236,22 +235,24 @@ function getBindsSync(app, addons) {
|
||||
return binds;
|
||||
}
|
||||
|
||||
function allocateOAuthCredentials(app, callback) {
|
||||
function setupOauth(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var appId = app.id;
|
||||
var id = 'cid-addon-' + uuid.v4();
|
||||
var id = 'cid-addon-oauth-' + uuid.v4();
|
||||
var clientSecret = hat(256);
|
||||
var redirectURI = 'https://' + config.appFqdn(app.location);
|
||||
var scope = 'profile,roleUser';
|
||||
|
||||
debugApp(app, 'allocateOAuthCredentials: id:%s clientSecret:%s', id, clientSecret);
|
||||
debugApp(app, 'setupOauth: id:%s clientSecret:%s', id, clientSecret);
|
||||
|
||||
clientdb.delByAppId('addon-' + appId, function (error) { // remove existing creds
|
||||
// ensure 'addon-oauth-' is in sync with oauth.js
|
||||
clientdb.delByAppId('addon-oauth-' + appId, function (error) { // remove existing creds
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
clientdb.add(id, 'addon-' + appId, clientSecret, redirectURI, scope, function (error) {
|
||||
clientdb.add(id, 'addon-oauth-' + appId, clientSecret, redirectURI, scope, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
@@ -267,29 +268,80 @@ function allocateOAuthCredentials(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeOAuthCredentials(app, callback) {
|
||||
function teardownOauth(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'removeOAuthCredentials');
|
||||
debugApp(app, 'teardownOauth');
|
||||
|
||||
clientdb.delByAppId('addon-' + app.id, function (error) {
|
||||
clientdb.delByAppId('addon-oauth-' + app.id, function (error) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'oauth', callback);
|
||||
});
|
||||
}
|
||||
|
||||
function setupLdap(app, callback) {
|
||||
function setupSimpleAuth(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var appId = app.id;
|
||||
var id = 'cid-addon-simpleauth-' + uuid.v4();
|
||||
var scope = 'profile,roleUser';
|
||||
|
||||
debugApp(app, 'setupSimpleAuth: id:%s', id);
|
||||
|
||||
// ensure 'addon-simpleauth-' is in sync with oauth.js
|
||||
clientdb.delByAppId('addon-simpleauth-' + appId, function (error) { // remove existing creds
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
clientdb.add(id, 'addon-simpleauth-' + appId, '', '', scope, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
'SIMPLE_AUTH_SERVER=172.17.42.1',
|
||||
'SIMPLE_AUTH_PORT=' + config.get('simpleAuthPort'),
|
||||
'SIMPLE_AUTH_URL=http://172.17.42.1:' + config.get('simpleAuthPort'), // obsolete, remove
|
||||
'SIMPLE_AUTH_ORIGIN=http://172.17.42.1:' + config.get('simpleAuthPort'),
|
||||
'SIMPLE_AUTH_CLIENT_ID=' + id
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting simple auth addon config to %j', env);
|
||||
|
||||
appdb.setAddonConfig(appId, 'simpleauth', env, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function teardownSimpleAuth(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'teardownSimpleAuth');
|
||||
|
||||
clientdb.delByAppId('addon-simpleauth-' + app.id, function (error) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'simpleauth', callback);
|
||||
});
|
||||
}
|
||||
|
||||
function setupLdap(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var env = [
|
||||
'LDAP_SERVER=172.17.42.1',
|
||||
'LDAP_PORT=3002',
|
||||
'LDAP_URL=ldap://172.17.42.1:3002',
|
||||
'LDAP_PORT=' + config.get('ldapPort'),
|
||||
'LDAP_URL=ldap://172.17.42.1:' + config.get('ldapPort'),
|
||||
'LDAP_USERS_BASE_DN=ou=users,dc=cloudron',
|
||||
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron'
|
||||
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron',
|
||||
'LDAP_BIND_DN=cn='+ app.id + ',ou=apps,dc=cloudron',
|
||||
'LDAP_BIND_PASSWORD=' + hat(256) // this is ignored
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up LDAP');
|
||||
@@ -297,8 +349,9 @@ function setupLdap(app, callback) {
|
||||
appdb.setAddonConfig(app.id, 'ldap', env, callback);
|
||||
}
|
||||
|
||||
function teardownLdap(app, callback) {
|
||||
function teardownLdap(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Tearing down LDAP');
|
||||
@@ -306,13 +359,14 @@ function teardownLdap(app, callback) {
|
||||
appdb.unsetAddonConfig(app.id, 'ldap', callback);
|
||||
}
|
||||
|
||||
function setupSendMail(app, callback) {
|
||||
function setupSendMail(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var env = [
|
||||
'MAIL_SMTP_SERVER=mail',
|
||||
'MAIL_SMTP_PORT=25',
|
||||
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
|
||||
'MAIL_SMTP_USERNAME=' + (app.location || app.id), // use app.id for bare domains
|
||||
'MAIL_DOMAIN=' + config.fqdn()
|
||||
];
|
||||
@@ -322,8 +376,9 @@ function setupSendMail(app, callback) {
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
}
|
||||
|
||||
function teardownSendMail(app, callback) {
|
||||
function teardownSendMail(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Tearing down sendmail');
|
||||
@@ -331,8 +386,9 @@ function teardownSendMail(app, callback) {
|
||||
appdb.unsetAddonConfig(app.id, 'sendmail', callback);
|
||||
}
|
||||
|
||||
function setupMySql(app, callback) {
|
||||
function setupMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Setting up mysql');
|
||||
@@ -365,7 +421,11 @@ function setupMySql(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function teardownMySql(app, callback) {
|
||||
function teardownMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = docker.getContainer('mysql');
|
||||
var cmd = [ '/addons/mysql/service.sh', 'remove', app.id ];
|
||||
|
||||
@@ -387,7 +447,7 @@ function teardownMySql(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function backupMySql(app, callback) {
|
||||
function backupMySql(app, options, callback) {
|
||||
debugApp(app, 'Backing up mysql');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
@@ -406,10 +466,10 @@ function backupMySql(app, callback) {
|
||||
cp.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
function restoreMySql(app, callback) {
|
||||
function restoreMySql(app, options, callback) {
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
setupMySql(app, function (error) {
|
||||
setupMySql(app, options, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'restoreMySql');
|
||||
@@ -431,8 +491,9 @@ function restoreMySql(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setupPostgreSql(app, callback) {
|
||||
function setupPostgreSql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Setting up postgresql');
|
||||
@@ -465,7 +526,11 @@ function setupPostgreSql(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function teardownPostgreSql(app, callback) {
|
||||
function teardownPostgreSql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = docker.getContainer('postgresql');
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'remove', app.id ];
|
||||
|
||||
@@ -487,7 +552,7 @@ function teardownPostgreSql(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function backupPostgreSql(app, callback) {
|
||||
function backupPostgreSql(app, options, callback) {
|
||||
debugApp(app, 'Backing up postgresql');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
@@ -506,10 +571,10 @@ function backupPostgreSql(app, callback) {
|
||||
cp.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
function restorePostgreSql(app, callback) {
|
||||
function restorePostgreSql(app, options, callback) {
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
setupPostgreSql(app, function (error) {
|
||||
setupPostgreSql(app, options, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'restorePostgreSql');
|
||||
@@ -531,8 +596,9 @@ function restorePostgreSql(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setupMongoDb(app, callback) {
|
||||
function setupMongoDb(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Setting up mongodb');
|
||||
@@ -565,7 +631,11 @@ function setupMongoDb(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function teardownMongoDb(app, callback) {
|
||||
function teardownMongoDb(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = docker.getContainer('mongodb');
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'remove', app.id ];
|
||||
|
||||
@@ -587,7 +657,7 @@ function teardownMongoDb(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function backupMongoDb(app, callback) {
|
||||
function backupMongoDb(app, options, callback) {
|
||||
debugApp(app, 'Backing up mongodb');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
@@ -606,10 +676,10 @@ function backupMongoDb(app, callback) {
|
||||
cp.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
function restoreMongoDb(app, callback) {
|
||||
function restoreMongoDb(app, options, callback) {
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
setupMongoDb(app, function (error) {
|
||||
setupMongoDb(app, options, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'restoreMongoDb');
|
||||
@@ -648,8 +718,30 @@ function forwardRedisPort(appId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function stopAndRemoveRedis(container, callback) {
|
||||
function ignoreError(func) {
|
||||
return function (callback) {
|
||||
func(function (error) {
|
||||
if (error) debug('stopAndRemoveRedis: Ignored error:', error);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// stopping redis with SIGTERM makes it commit the database to disk
|
||||
async.series([
|
||||
ignoreError(container.stop.bind(container, { t: 10 })),
|
||||
ignoreError(container.wait.bind(container)),
|
||||
ignoreError(container.remove.bind(container, { force: true, v: true }))
|
||||
], callback);
|
||||
}
|
||||
|
||||
// Ensures that app's addon redis container is running. Can be called when named container already exists/running
|
||||
function setupRedis(app, callback) {
|
||||
function setupRedis(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var redisPassword = generatePassword(64, false /* memorable */);
|
||||
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
var redisDataDir = path.join(paths.DATA_DIR, app.id + '/redis');
|
||||
@@ -664,9 +756,13 @@ function setupRedis(app, callback) {
|
||||
name: 'redis-' + app.id,
|
||||
Hostname: config.appFqdn(app.location),
|
||||
Tty: true,
|
||||
Image: 'cloudron/redis:0.3.2', // if you change this, fix setup/INFRA_VERSION as well
|
||||
Image: 'cloudron/redis:0.6.1', // if you change this, fix setup/INFRA_VERSION as well
|
||||
Cmd: null,
|
||||
Volumes: {},
|
||||
Volumes: {
|
||||
'/tmp': {},
|
||||
'/run': {},
|
||||
'/var/log': {}
|
||||
},
|
||||
VolumesFrom: []
|
||||
};
|
||||
|
||||
@@ -677,11 +773,14 @@ function setupRedis(app, callback) {
|
||||
redisVarsFile + ':/etc/redis/redis_vars.sh:ro',
|
||||
redisDataDir + ':/var/lib/redis:rw'
|
||||
],
|
||||
Memory: 1024 * 1024 * 75, // 100mb
|
||||
MemorySwap: 1024 * 1024 * 75 * 2, // 150mb
|
||||
// On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work
|
||||
// On linux, export to localhost only for testing purposes and not for the app itself
|
||||
PortBindings: {
|
||||
'6379/tcp': [{ HostPort: '0', HostIp: isMac ? '0.0.0.0' : '127.0.0.1' }]
|
||||
},
|
||||
ReadonlyRootfs: true,
|
||||
RestartPolicy: {
|
||||
'Name': 'always',
|
||||
'MaximumRetryCount': 0
|
||||
@@ -696,7 +795,7 @@ function setupRedis(app, callback) {
|
||||
];
|
||||
|
||||
var redisContainer = docker.getContainer(createOptions.name);
|
||||
redisContainer.remove({ force: true, v: false }, function (ignoredError) {
|
||||
stopAndRemoveRedis(redisContainer, function () {
|
||||
docker.createContainer(createOptions, function (error) {
|
||||
if (error && error.statusCode !== 409) return callback(error); // if not already created
|
||||
|
||||
@@ -713,12 +812,16 @@ function setupRedis(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function teardownRedis(app, callback) {
|
||||
function teardownRedis(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = docker.getContainer('redis-' + app.id);
|
||||
|
||||
var removeOptions = {
|
||||
force: true, // kill container if it's running
|
||||
v: false // removes volumes associated with the container
|
||||
v: true // removes volumes associated with the container
|
||||
};
|
||||
|
||||
container.remove(removeOptions, function (error) {
|
||||
@@ -736,40 +839,18 @@ function teardownRedis(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function allocateAccessToken(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
function backupRedis(app, options, callback) {
|
||||
debugApp(app, 'Backing up redis');
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var expiresAt = Number.MAX_SAFE_INTEGER; // basically never expire
|
||||
var scopes = 'profile,users'; // TODO This should be put into the manifest and the user should know those
|
||||
var clientId = ''; // meaningless for apps so far
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
tokendb.delByIdentifier(tokendb.PREFIX_APP + app.id, function (error) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
tokendb.add(token, tokendb.PREFIX_APP + app.id, clientId, expiresAt, scopes, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
'CLOUDRON_TOKEN=' + token
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting token addon config to %j', env);
|
||||
|
||||
appdb.setAddonConfig(appId, 'token', env, callback);
|
||||
});
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', 'redis-' + app.id, '/addons/redis/service.sh', 'backup' ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'backupRedis: done. code:%s signal:%s', code, signal);
|
||||
if (!callback.called) callback(code ? 'backupRedis failed with status ' + code : null);
|
||||
});
|
||||
|
||||
cp.stdout.pipe(process.stdout);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
function removeAccessToken(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.delByIdentifier(tokendb.PREFIX_APP + app.id, function (error) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'token', callback);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+25
-6
@@ -6,6 +6,7 @@ exports = module.exports = {
|
||||
get: get,
|
||||
getBySubdomain: getBySubdomain,
|
||||
getByHttpPort: getByHttpPort,
|
||||
getByContainerId: getByContainerId,
|
||||
add: add,
|
||||
exists: exists,
|
||||
del: del,
|
||||
@@ -39,7 +40,6 @@ exports = module.exports = {
|
||||
RSTATE_PENDING_START: 'pending_start',
|
||||
RSTATE_PENDING_STOP: 'pending_stop',
|
||||
RSTATE_STOPPED: 'stopped', // app stopped by use
|
||||
RSTATE_ERROR: 'error',
|
||||
|
||||
// run codes (keep in sync in UI)
|
||||
HEALTH_HEALTHY: 'healthy',
|
||||
@@ -59,11 +59,11 @@ var assert = require('assert'),
|
||||
|
||||
var APPS_FIELDS = [ 'id', 'appStoreId', 'installationState', 'installationProgress', 'runState',
|
||||
'health', 'containerId', 'manifestJson', 'httpPort', 'location', 'dnsRecordId',
|
||||
'accessRestriction', 'lastBackupId', 'lastBackupConfigJson', 'oldConfigJson' ].join(',');
|
||||
'accessRestriction', 'lastBackupId', 'lastBackupConfigJson', 'oldConfigJson', 'oauthProxy' ].join(',');
|
||||
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
|
||||
'apps.accessRestriction', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson' ].join(',');
|
||||
'apps.accessRestriction', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.oauthProxy' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
@@ -95,6 +95,8 @@ function postProcess(result) {
|
||||
for (var i = 0; i < environmentVariables.length; i++) {
|
||||
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
|
||||
}
|
||||
|
||||
result.oauthProxy = !!result.oauthProxy;
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
@@ -145,6 +147,22 @@ function getByHttpPort(httpPort, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByContainerId(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
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'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE containerId = ? GROUP BY apps.id', [ 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));
|
||||
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -160,7 +178,7 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, callback) {
|
||||
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
@@ -168,6 +186,7 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof accessRestriction, 'string');
|
||||
assert.strictEqual(typeof oauthProxy, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
portBindings = portBindings || { };
|
||||
@@ -176,8 +195,8 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
|
||||
|
||||
var queries = [ ];
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestriction) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestriction ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestriction, oauthProxy) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestriction, oauthProxy ]
|
||||
});
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
|
||||
Executable → Regular
+51
-3
@@ -1,7 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
@@ -21,6 +19,7 @@ var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be smal
|
||||
var UNHEALTHY_THRESHOLD = 3 * 60 * 1000; // 3 minutes
|
||||
var gHealthInfo = { }; // { time, emailSent }
|
||||
var gRunTimeout = null;
|
||||
var gDockerEventStream = null;
|
||||
|
||||
function debugApp(app) {
|
||||
assert(!app || typeof app === 'object');
|
||||
@@ -47,7 +46,7 @@ function setHealth(app, health, callback) {
|
||||
|
||||
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
|
||||
|
||||
mailer.appDied(app);
|
||||
if (app.appStoreId !== '') mailer.appDied(app); // do not send mails for dev apps
|
||||
gHealthInfo[app.id].emailSent = true;
|
||||
} else {
|
||||
debugApp(app, 'waiting for sometime to update the app health');
|
||||
@@ -124,11 +123,58 @@ function run() {
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
OOM can be tested using stress tool like so:
|
||||
docker run -ti -m 100M cloudron/base:0.3.3 /bin/bash
|
||||
apt-get update && apt-get install stress
|
||||
stress --vm 1 --vm-bytes 200M --vm-hang 0
|
||||
*/
|
||||
function processDockerEvents() {
|
||||
// note that for some reason, the callback is called only on the first event
|
||||
debug('Listening for docker events');
|
||||
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
gDockerEventStream = stream;
|
||||
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (data) {
|
||||
var ev = JSON.parse(data);
|
||||
debug('Container ' + ev.id + ' went OOM');
|
||||
appdb.getByContainerId(ev.id, function (error, app) {
|
||||
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
|
||||
var context = JSON.stringify(ev);
|
||||
if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n';
|
||||
|
||||
debug('OOM Context: %s', context);
|
||||
|
||||
// do not send mails for dev apps
|
||||
if (app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('error', function (error) {
|
||||
console.error('Error reading docker events', error);
|
||||
gDockerEventStream = null; // TODO: reconnect?
|
||||
});
|
||||
|
||||
stream.on('end', function () {
|
||||
console.error('Docke event stream ended');
|
||||
gDockerEventStream = null; // TODO: reconnect?
|
||||
stream.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Starting apphealthmonitor');
|
||||
|
||||
processDockerEvents();
|
||||
|
||||
run();
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -136,5 +182,7 @@ function stop(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clearTimeout(gRunTimeout);
|
||||
gDockerEventStream.end();
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
+79
-33
@@ -150,6 +150,9 @@ function validatePortBindings(portBindings, tcpPorts) {
|
||||
2020, /* install server */
|
||||
config.get('port'), /* app server (lo) */
|
||||
config.get('internalPort'), /* internal app server (lo) */
|
||||
config.get('ldapPort'), /* ldap server (lo) */
|
||||
config.get('oauthProxyPort'), /* oauth proxy server (lo) */
|
||||
config.get('simpleAuthPort'), /* simple auth server (lo) */
|
||||
3306, /* mysql (lo) */
|
||||
8000 /* graphite (lo) */
|
||||
];
|
||||
@@ -278,13 +281,14 @@ function purchase(appStoreId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, callback) {
|
||||
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, icon, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof accessRestriction, 'string');
|
||||
assert.strictEqual(typeof oauthProxy, 'boolean');
|
||||
assert(!icon || typeof icon === 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -316,7 +320,7 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
purchase(appStoreId, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, function (error) {
|
||||
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, oauthProxy, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -327,11 +331,12 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
});
|
||||
}
|
||||
|
||||
function configure(appId, location, portBindings, accessRestriction, callback) {
|
||||
function configure(appId, location, portBindings, accessRestriction, oauthProxy, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof accessRestriction, 'string');
|
||||
assert.strictEqual(typeof oauthProxy, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateHostname(location, config.fqdn());
|
||||
@@ -350,12 +355,14 @@ function configure(appId, location, portBindings, accessRestriction, callback) {
|
||||
var values = {
|
||||
location: location.toLowerCase(),
|
||||
accessRestriction: accessRestriction,
|
||||
oauthProxy: oauthProxy,
|
||||
portBindings: portBindings,
|
||||
|
||||
oldConfig: {
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings
|
||||
portBindings: app.portBindings,
|
||||
oauthProxy: app.oauthProxy
|
||||
}
|
||||
};
|
||||
|
||||
@@ -509,6 +516,7 @@ function restore(appId, callback) {
|
||||
oldConfig: {
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
oauthProxy: app.oauthProxy,
|
||||
portBindings: app.portBindings,
|
||||
manifest: app.manifest
|
||||
}
|
||||
@@ -688,33 +696,35 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function backupApp(app, addonsToBackup, callback) {
|
||||
function canBackupApp(app) {
|
||||
// only backup apps that are installed or pending configure or called from apptask. Rest of them are in some
|
||||
// state not good for consistent backup (i.e addons may not have been setup completely)
|
||||
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
|
||||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
|
||||
app.installationState === appdb.ISTATE_PENDING_BACKUP || // called from apptask
|
||||
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
|
||||
}
|
||||
|
||||
// set the 'creation' date of lastBackup so that the backup persists across time based archival rules
|
||||
// s3 does not allow changing creation time, so copying the last backup is easy way out for now
|
||||
function reuseOldBackup(app, callback) {
|
||||
assert.strictEqual(typeof app.lastBackupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
backups.copyLastBackup(app, function (error, newBackupId) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
debugApp(app, 'reuseOldBackup: reused old backup %s as %s', app.lastBackupId, newBackupId);
|
||||
|
||||
callback(null, newBackupId);
|
||||
});
|
||||
}
|
||||
|
||||
function createNewBackup(app, addonsToBackup, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addonsToBackup || typeof addonsToBackup, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function canBackupApp(app) {
|
||||
// only backup apps that are installed or pending configure. Rest of them are in some
|
||||
// state not good for consistent backup (i.e addons may not have been setup completely)
|
||||
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
|
||||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
|
||||
app.installationState === appdb.ISTATE_PENDING_BACKUP ||
|
||||
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
|
||||
}
|
||||
|
||||
if (!canBackupApp(app)) return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy'));
|
||||
|
||||
var appConfig = {
|
||||
manifest: app.manifest,
|
||||
location: app.location,
|
||||
portBindings: app.portBindings,
|
||||
accessRestriction: app.accessRestriction
|
||||
};
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
backups.getBackupUrl(app, function (error, result) {
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
@@ -729,13 +739,50 @@ function backupApp(app, addonsToBackup, callback) {
|
||||
], function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
debugApp(app, 'backupApp: successful id:%s', result.id);
|
||||
callback(null, result.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setRestorePoint(app.id, result.id, appConfig, function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
function backupApp(app, addonsToBackup, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addonsToBackup || typeof addonsToBackup, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
return callback(null, result.id);
|
||||
});
|
||||
var appConfig = null, backupFunction;
|
||||
|
||||
if (!canBackupApp(app)) {
|
||||
if (!app.lastBackupId) {
|
||||
debugApp(app, 'backupApp: cannot backup app');
|
||||
return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy and never backed up previously'));
|
||||
}
|
||||
|
||||
appConfig = app.lastBackupConfig;
|
||||
backupFunction = reuseOldBackup.bind(null, app);
|
||||
} else {
|
||||
appConfig = {
|
||||
manifest: app.manifest,
|
||||
location: app.location,
|
||||
portBindings: app.portBindings,
|
||||
accessRestriction: app.accessRestriction,
|
||||
oauthProxy: app.oauthProxy
|
||||
};
|
||||
backupFunction = createNewBackup.bind(null, app, addonsToBackup);
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
|
||||
return callback(safe.error);
|
||||
}
|
||||
}
|
||||
|
||||
backupFunction(function (error, backupId) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
debugApp(app, 'backupApp: successful id:%s', backupId);
|
||||
|
||||
setRestorePoint(app.id, backupId, appConfig, function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, backupId);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -778,4 +825,3 @@ function restoreApp(app, addonsToRestore, callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+66
-34
@@ -25,6 +25,12 @@ exports = module.exports = {
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs() {
|
||||
arguments[0] = this.namespace + ' ' + arguments[0];
|
||||
return arguments;
|
||||
};
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
@@ -45,6 +51,7 @@ var addons = require('./addons.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
shell = require('./shell.js'),
|
||||
SubdomainError = require('./subdomainerror.js'),
|
||||
subdomains = require('./subdomains.js'),
|
||||
@@ -73,6 +80,14 @@ function debugApp(app, args) {
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function targetBoxVersion(manifest) {
|
||||
if ('targetBoxVersion' in manifest) return manifest.targetBoxVersion;
|
||||
|
||||
if ('minBoxVersion' in manifest) return manifest.minBoxVersion;
|
||||
|
||||
return '0.0.1';
|
||||
}
|
||||
|
||||
// We expect conflicts to not happen despite closing the port (parallel app installs, app update does not reconfigure nginx etc)
|
||||
// https://tools.ietf.org/html/rfc6056#section-3.5 says linux uses random ephemeral port allocation
|
||||
function getFreePort(callback) {
|
||||
@@ -94,7 +109,7 @@ function configureNginx(app, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var sourceDir = path.resolve(__dirname, '..');
|
||||
var endpoint = app.accessRestriction ? 'oauthproxy' : 'app';
|
||||
var endpoint = app.oauthProxy ? 'oauthproxy' : 'app';
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, adminOrigin: config.adminOrigin(), vhost: config.appFqdn(app.location), port: freePort, endpoint: endpoint });
|
||||
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
@@ -126,23 +141,21 @@ function unconfigureNginx(app, callback) {
|
||||
vbox.unforwardFromHostToVirtualBox(app.id + '-http');
|
||||
}
|
||||
|
||||
function downloadImage(app, callback) {
|
||||
debugApp(app, 'downloadImage %s', app.manifest.dockerImage);
|
||||
|
||||
function pullImage(app, callback) {
|
||||
docker.pull(app.manifest.dockerImage, function (err, stream) {
|
||||
if (err) return callback(new Error('Error connecting to docker'));
|
||||
if (err) return callback(new Error('Error connecting to docker. statusCode: %s' + err.statusCode));
|
||||
|
||||
// https://github.com/dotcloud/docker/issues/1074 says each status message
|
||||
// is emitted as a chunk
|
||||
stream.on('data', function (chunk) {
|
||||
var data = safe.JSON.parse(chunk) || { };
|
||||
debugApp(app, 'downloadImage data: %j', data);
|
||||
debugApp(app, 'pullImage data: %j', data);
|
||||
|
||||
// The information here is useless because this is per layer as opposed to per image
|
||||
if (data.status) {
|
||||
debugApp(app, 'progress: %s', data.status); // progressDetail { current, total }
|
||||
// debugApp(app, 'progress: %s', data.status); // progressDetail { current, total }
|
||||
} else if (data.error) {
|
||||
debugApp(app, 'error detail: %s', data.errorDetail.message);
|
||||
debugApp(app, 'pullImage error detail: %s', data.errorDetail.message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -152,25 +165,40 @@ function downloadImage(app, callback) {
|
||||
var image = docker.getImage(app.manifest.dockerImage);
|
||||
|
||||
image.inspect(function (err, data) {
|
||||
if (err) {
|
||||
return callback(new Error('Error inspecting image:' + err.message));
|
||||
}
|
||||
|
||||
if (!data || !data.Config) {
|
||||
return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
|
||||
}
|
||||
|
||||
if (!data.Config.Entrypoint && !data.Config.Cmd) {
|
||||
return callback(new Error('Only images with entry point are allowed'));
|
||||
}
|
||||
if (err) return callback(new Error('Error inspecting image:' + err.message));
|
||||
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
|
||||
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
|
||||
|
||||
debugApp(app, 'This image exposes ports: %j', data.Config.ExposedPorts);
|
||||
return callback(null);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('error', function (error) {
|
||||
debugApp(app, 'pullImage error : %j', error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function downloadImage(app, callback) {
|
||||
debugApp(app, 'downloadImage %s', app.manifest.dockerImage);
|
||||
|
||||
var attempt = 1;
|
||||
|
||||
async.retry({ times: 5, interval: 15000 }, function (retryCallback) {
|
||||
debugApp(app, 'Downloading image. attempt: %s', attempt++);
|
||||
|
||||
pullImage(app, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function createContainer(app, callback) {
|
||||
appdb.getPortBindings(app.id, function (error, portBindings) {
|
||||
if (error) return callback(error);
|
||||
@@ -204,7 +232,12 @@ function createContainer(app, callback) {
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: null,
|
||||
Env: env.concat(addonEnv),
|
||||
ExposedPorts: exposedPorts
|
||||
ExposedPorts: exposedPorts,
|
||||
Volumes: { // see also ReadonlyRootfs
|
||||
'/tmp': {},
|
||||
'/run': {},
|
||||
'/var/log': {}
|
||||
}
|
||||
};
|
||||
|
||||
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
|
||||
@@ -225,7 +258,7 @@ function deleteContainer(app, callback) {
|
||||
|
||||
var removeOptions = {
|
||||
force: true, // kill container if it's running
|
||||
v: false // removes volumes associated with the container
|
||||
v: true // removes volumes associated with the container (but not host mounts)
|
||||
};
|
||||
|
||||
container.remove(removeOptions, function (error) {
|
||||
@@ -274,13 +307,13 @@ function allocateOAuthProxyCredentials(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!app.accessRestriction) return callback(null);
|
||||
if (!app.oauthProxy) return callback(null);
|
||||
|
||||
var appId = 'proxy-' + app.id;
|
||||
var id = 'cid-proxy-' + uuid.v4();
|
||||
var clientSecret = hat(256);
|
||||
var redirectURI = 'https://' + config.appFqdn(app.location);
|
||||
var scope = 'profile,' + app.accessRestriction;
|
||||
var scope = 'profile,roleUser';
|
||||
|
||||
clientdb.add(id, appId, clientSecret, redirectURI, scope, callback);
|
||||
}
|
||||
@@ -341,12 +374,14 @@ function startContainer(app, callback) {
|
||||
MemorySwap: memoryLimit, // Memory + Swap
|
||||
PortBindings: dockerPortBindings,
|
||||
PublishAllPorts: false,
|
||||
ReadonlyRootfs: semver.gte(targetBoxVersion(app.manifest), '0.0.66'), // see also Volumes in startContainer
|
||||
Links: addons.getLinksSync(app, app.manifest.addons),
|
||||
RestartPolicy: {
|
||||
"Name": "always",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
CpuShares: 512 // relative to 1024 for system processes
|
||||
CpuShares: 512, // relative to 1024 for system processes
|
||||
SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron
|
||||
};
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
@@ -429,11 +464,11 @@ function registerSubdomain(app, callback) {
|
||||
// need to register it so that we have a dnsRecordId to wait for it to complete
|
||||
var record = { subdomain: app.location, type: 'A', value: sysinfo.getIp() };
|
||||
|
||||
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
|
||||
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Registering subdomain location [%s]', app.location);
|
||||
|
||||
subdomains.add(record, function (error, changeId) {
|
||||
if (error && error.reason === SubdomainError.STILL_BUSY) return retryCallback(error); // try again
|
||||
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
|
||||
retryCallback(null, error || changeId);
|
||||
});
|
||||
@@ -457,9 +492,9 @@ function unregisterSubdomain(app, location, callback) {
|
||||
debugApp(app, 'Unregistering subdomain: %s', location);
|
||||
|
||||
subdomains.remove(record, function (error) {
|
||||
if (error && error.reason === SubdomainError.STILL_BUSY) return retryCallback(error); // try again
|
||||
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR))return retryCallback(error); // try again
|
||||
|
||||
retryCallback(null);
|
||||
retryCallback(error);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) debugApp(app, 'Error unregistering subdomain: %s', error);
|
||||
@@ -499,7 +534,7 @@ function waitForDnsPropagation(app, callback) {
|
||||
|
||||
// updates the app object and the database
|
||||
function updateApp(app, values, callback) {
|
||||
debugApp(app, 'installationState: %s progress: %s', app.installationState, app.installationProgress);
|
||||
debugApp(app, 'updating app with values: %j', values);
|
||||
|
||||
appdb.update(app.id, values, function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -835,10 +870,7 @@ function uninstall(app, callback) {
|
||||
|
||||
function runApp(app, callback) {
|
||||
startContainer(app, function (error) {
|
||||
if (error) {
|
||||
debugApp(app, 'Error starting container : %s', error);
|
||||
return updateApp(app, { runState: appdb.RSTATE_ERROR }, callback);
|
||||
}
|
||||
if (error) return callback(error);
|
||||
|
||||
updateApp(app, { runState: appdb.RSTATE_RUNNING }, callback);
|
||||
});
|
||||
|
||||
+22
-1
@@ -8,7 +8,9 @@ exports = module.exports = {
|
||||
|
||||
addSubdomain: addSubdomain,
|
||||
delSubdomain: delSubdomain,
|
||||
getChangeStatus: getChangeStatus
|
||||
getChangeStatus: getChangeStatus,
|
||||
|
||||
copyObject: copyObject
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -259,3 +261,22 @@ function getChangeStatus(changeId, callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyObject(from, to, callback) {
|
||||
assert.strictEqual(typeof from, 'string');
|
||||
assert.strictEqual(typeof to, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var params = {
|
||||
Bucket: config.aws().backupBucket, // target bucket
|
||||
Key: config.aws().backupPrefix + '/' + to, // target file
|
||||
CopySource: config.aws().backupBucket + '/' + config.aws().backupPrefix + '/' + from, // source
|
||||
};
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
s3.copyObject(params, callback);
|
||||
});
|
||||
}
|
||||
|
||||
+18
-3
@@ -6,7 +6,9 @@ exports = module.exports = {
|
||||
getAllPaged: getAllPaged,
|
||||
|
||||
getBackupUrl: getBackupUrl,
|
||||
getRestoreUrl: getRestoreUrl
|
||||
getRestoreUrl: getRestoreUrl,
|
||||
|
||||
copyLastBackup: copyLastBackup
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -76,7 +78,7 @@ function getBackupUrl(app, callback) {
|
||||
backupKey: config.backupKey()
|
||||
};
|
||||
|
||||
debug('getBackupUrl: ', obj);
|
||||
debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
|
||||
|
||||
callback(null, obj);
|
||||
});
|
||||
@@ -97,8 +99,21 @@ function getRestoreUrl(backupId, callback) {
|
||||
backupKey: config.backupKey()
|
||||
};
|
||||
|
||||
debug('getRestoreUrl: ', obj);
|
||||
debug('getRestoreUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
|
||||
|
||||
callback(null, obj);
|
||||
});
|
||||
}
|
||||
|
||||
function copyLastBackup(app, callback) {
|
||||
assert(app && typeof app === 'object');
|
||||
assert.strictEqual(typeof app.lastBackupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var toFilename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
|
||||
aws.copyObject(app.lastBackupId, toFilename, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, toFilename);
|
||||
});
|
||||
}
|
||||
|
||||
+7
-2
@@ -22,7 +22,9 @@ function addSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('addSubdomain: ' + subdomain + ' for domain ' + zoneName + ' with value ' + value);
|
||||
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
|
||||
|
||||
debug('addSubdomain: zoneName: %s subdomain: %s type: %s value: %s fqdn: %s', zoneName, subdomain, type, value, fqdn);
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
@@ -30,11 +32,12 @@ function addSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
};
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain))
|
||||
.post(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
|
||||
.query({ token: config.token() })
|
||||
.send(data)
|
||||
.end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.status === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
|
||||
if (result.status !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
return callback(null, result.body.changeId);
|
||||
@@ -61,6 +64,8 @@ function delSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
.send(data)
|
||||
.end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.status === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
|
||||
if (result.status === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND));
|
||||
if (result.status !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
return callback(null);
|
||||
|
||||
+24
-28
@@ -19,7 +19,8 @@ exports = module.exports = {
|
||||
reboot: reboot,
|
||||
migrate: migrate,
|
||||
backup: backup,
|
||||
ensureBackup: ensureBackup};
|
||||
ensureBackup: ensureBackup
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
AppsError = require('./apps.js').AppsError,
|
||||
@@ -138,7 +139,7 @@ function setTimeZone(ip, callback) {
|
||||
}
|
||||
|
||||
if (!result.body.timezone) {
|
||||
debug('No timezone in geoip response');
|
||||
debug('No timezone in geoip response : %j', result.body);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
@@ -158,7 +159,7 @@ function activate(username, password, email, name, ip, callback) {
|
||||
|
||||
debug('activating user:%s email:%s', username, email);
|
||||
|
||||
setTimeZone(ip, function () { });
|
||||
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
|
||||
|
||||
if (!name) name = settings.getDefaultSync(settings.CLOUDRON_NAME_KEY);
|
||||
|
||||
@@ -323,31 +324,23 @@ function addDnsRecords() {
|
||||
function checkIfInSync() {
|
||||
debug('addDnsRecords: Check if admin DNS record is in sync.');
|
||||
|
||||
var allDone = true;
|
||||
|
||||
async.each(changeIds, function (changeId, callback) {
|
||||
async.eachSeries(changeIds, function (changeId, callback) {
|
||||
subdomains.status(changeId, function (error, result) {
|
||||
if (error) return callback(new Error('Failed to check if admin DNS record is in sync.', error));
|
||||
|
||||
if (result !== 'done') allDone = false;
|
||||
if (result !== 'done') return callback(new Error(changeId + ' is not in sync. result:' + result));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// retry if needed
|
||||
if (error || !allDone) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
gAddDnsRecordsTimerId = setTimeout(checkIfInSync, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
config.set('dnsInSync', true);
|
||||
|
||||
// send heartbeat after the dns records are done
|
||||
sendHeartbeat();
|
||||
|
||||
debug('addDnsRecords: done');
|
||||
config.set('dnsInSync', true);
|
||||
sendHeartbeat(); // send heartbeat after the dns records are done
|
||||
});
|
||||
}
|
||||
|
||||
@@ -463,7 +456,7 @@ function doUpgrade(boxUpdateInfo, callback) {
|
||||
callback(e);
|
||||
}
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Create app and box backup for upgrade');
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
|
||||
|
||||
backupBoxAndApps(function (error) {
|
||||
if (error) return upgradeError(error);
|
||||
@@ -473,7 +466,7 @@ function doUpgrade(boxUpdateInfo, callback) {
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.end(function (error, result) {
|
||||
if (error) return upgradeError(new Error('Error making upgrade request: ' + error));
|
||||
if (result.status !== 202) return upgradeError(new Error('Server not ready to upgrade: ' + result.body));
|
||||
if (result.status !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
|
||||
@@ -492,9 +485,9 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
callback(e);
|
||||
}
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Create box backup for update');
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for update');
|
||||
|
||||
backupBox(function (error) {
|
||||
backupBoxAndApps(function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// fetch a signed sourceTarballUrl
|
||||
@@ -503,7 +496,7 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
.end(function (error, result) {
|
||||
if (error) return updateError(new Error('Error fetching sourceTarballUrl: ' + error));
|
||||
if (result.status !== 200) return updateError(new Error('Error fetching sourceTarballUrl status: ' + result.status));
|
||||
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + result.body));
|
||||
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + JSON.stringify(result.body)));
|
||||
|
||||
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
|
||||
var args = {
|
||||
@@ -531,7 +524,7 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
|
||||
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
|
||||
if (error) return updateError(error);
|
||||
if (result.status !== 202) return updateError(new Error('Error initiating update: ' + result.body));
|
||||
if (result.status !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
|
||||
|
||||
@@ -549,6 +542,9 @@ function backup(callback) {
|
||||
var error = locker.lock(locker.OP_FULL_BACKUP);
|
||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
||||
|
||||
// clearing backup ensures tools can 'wait' on progress
|
||||
progress.clear(progress.BACKUP);
|
||||
|
||||
// start the backup operation in the background
|
||||
backupBoxAndApps(function (error) {
|
||||
if (error) console.error('backup failed.', error);
|
||||
@@ -633,12 +629,12 @@ function backupBoxAndApps(callback) {
|
||||
apps.backupApp(app, app.manifest.addons, function (error, backupId) {
|
||||
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
|
||||
|
||||
if (error && error.reason === AppsError.BAD_STATE) {
|
||||
debugApp(app, 'Skipping backup (istate:%s health:%s). using lastBackupId:%s', app.installationState, app.health, app.lastBackupId);
|
||||
backupId = app.lastBackupId;
|
||||
if (error && error.reason !== AppsError.BAD_STATE) {
|
||||
debugApp(app, 'Unable to backup', error);
|
||||
return iteratorCallback(error);
|
||||
}
|
||||
|
||||
return iteratorCallback(null, backupId);
|
||||
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
|
||||
});
|
||||
}, function appsBackedUp(error, backupIds) {
|
||||
if (error) {
|
||||
@@ -646,7 +642,7 @@ function backupBoxAndApps(callback) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
|
||||
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
|
||||
|
||||
backupBoxWithAppBackupIds(backupIds, function (error, restoreKey) {
|
||||
progress.set(progress.BACKUP, 100, error ? error.message : '');
|
||||
|
||||
@@ -25,6 +25,7 @@ exports = module.exports = {
|
||||
|
||||
// these values are derived
|
||||
adminOrigin: adminOrigin,
|
||||
internalAdminOrigin: internalAdminOrigin,
|
||||
appFqdn: appFqdn,
|
||||
zoneName: zoneName,
|
||||
|
||||
@@ -73,6 +74,8 @@ function initConfig() {
|
||||
data.webServerOrigin = null;
|
||||
data.internalPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
data.oauthProxyPort = 3003;
|
||||
data.simpleAuthPort = 3004;
|
||||
data.backupKey = 'backupKey';
|
||||
data.aws = {
|
||||
backupBucket: null,
|
||||
@@ -162,6 +165,10 @@ function adminOrigin() {
|
||||
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
|
||||
}
|
||||
|
||||
function internalAdminOrigin() {
|
||||
return 'http://127.0.0.1:' + get('port');
|
||||
}
|
||||
|
||||
function token() {
|
||||
return get('token');
|
||||
}
|
||||
|
||||
+3
-1
@@ -48,6 +48,8 @@ function recreateJobs(unusedTimeZone, callback) {
|
||||
if (typeof unusedTimeZone === 'function') callback = unusedTimeZone;
|
||||
|
||||
settings.getAll(function (error, allSettings) {
|
||||
debug('Creating jobs with timezone %s', allSettings[settings.TIME_ZONE_KEY]);
|
||||
|
||||
if (gHeartbeatJob) gHeartbeatJob.stop();
|
||||
gHeartbeatJob = new CronJob({
|
||||
cronTime: '00 */1 * * * *', // every minute
|
||||
@@ -110,7 +112,7 @@ function autoupdatePatternChanged(pattern) {
|
||||
}
|
||||
},
|
||||
start: true,
|
||||
timeZone: gBoxUpdateCheckerJob.cronTime.timeZone // hack
|
||||
timeZone: gBoxUpdateCheckerJob.cronTime.zone // hack
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+8
-4
@@ -63,7 +63,6 @@ function start(callback) {
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
|
||||
res.send(tmp);
|
||||
debug('ldap user send:', tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -100,7 +99,6 @@ function start(callback) {
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
|
||||
res.send(tmp);
|
||||
debug('ldap group send:', tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,8 +106,14 @@ function start(callback) {
|
||||
});
|
||||
});
|
||||
|
||||
gServer.bind('dc=cloudron', function(req, res, next) {
|
||||
debug('ldap bind: %s', req.dn.toString());
|
||||
gServer.bind('ou=apps,dc=cloudron', function(req, res, next) {
|
||||
// TODO: validate password
|
||||
debug('ldap application bind: %s', req.dn.toString());
|
||||
res.end();
|
||||
});
|
||||
|
||||
gServer.bind('ou=users,dc=cloudron', function(req, res, next) {
|
||||
debug('ldap user bind: %s', req.dn.toString());
|
||||
|
||||
if (!req.dn.rdns[0].cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dear Cloudron Team,
|
||||
|
||||
unfortunately the <%= program %> on <%= fqdn %> crashed unexpectedly!
|
||||
Unfortunately <%= program %> on <%= fqdn %> crashed unexpectedly!
|
||||
|
||||
Please see some excerpt of the logs below.
|
||||
|
||||
@@ -11,7 +11,7 @@ Your Cloudron
|
||||
|
||||
-------------------------------------
|
||||
|
||||
<%= context %>
|
||||
<%- context %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
|
||||
+2
-2
@@ -87,13 +87,13 @@ function processQueue() {
|
||||
|
||||
var transport = nodemailer.createTransport(smtpTransport({
|
||||
host: mailServerIp,
|
||||
port: 25
|
||||
port: 2500 // this value comes from mail container
|
||||
}));
|
||||
|
||||
var mailQueueCopy = gMailQueue;
|
||||
gMailQueue = [ ];
|
||||
|
||||
debug('Processing mail queue of size %d', mailQueueCopy.length);
|
||||
debug('Processing mail queue of size %d (through %s:2500)', mailQueueCopy.length, mailServerIp);
|
||||
|
||||
async.mapSeries(mailQueueCopy, function iterator(mailOptions, callback) {
|
||||
transport.sendMail(mailOptions, function (error) {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<% include header %>
|
||||
|
||||
<form action="/api/v1/oauth/dialog/authorize/decision" method="post">
|
||||
<input name="transaction_id" type="hidden" value="<%= transactionID %>">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3"></div>
|
||||
<div class="col-md-6">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
Hi <%= user.username %>!
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<b><%= client.name %></b> is requesting access to your account.
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
Do you approve?
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<input class="btn btn-danger btn-outline" type="submit" value="Deny" name="cancel" id="deny"/>
|
||||
<input class="btn btn-success btn-outline" type="submit" value="Allow"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3"></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<% include footer %>
|
||||
@@ -26,3 +26,13 @@
|
||||
</head>
|
||||
|
||||
<body class="oauth">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-default navbar-static-top shadow" role="navigation" style="margin-bottom: 0">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<span class="navbar-brand navbar-brand-icon"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></span>
|
||||
<span class="navbar-brand"><%= cloudronName %></span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" src="<%= applicationLogo %>"/>
|
||||
<h1>Login to <%= applicationName %> on <%= cloudronName %></h1>
|
||||
<h1><small>Login to</small> <%= applicationName %></h1>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+12
-10
@@ -5,8 +5,6 @@ exports = module.exports = {
|
||||
stop: stop
|
||||
};
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
@@ -20,9 +18,6 @@ var appdb = require('./appdb.js'),
|
||||
url = require('url'),
|
||||
uuid = require('node-uuid');
|
||||
|
||||
// Allow self signed certs!
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
var gSessions = {};
|
||||
var gProxyMiddlewareCache = {};
|
||||
var gHttpServer = null;
|
||||
@@ -51,7 +46,11 @@ function verifySession(req, res, next) {
|
||||
return next();
|
||||
}
|
||||
|
||||
superagent.get(config.adminOrigin() + '/api/v1/profile').query({ access_token: req.sessionData.accessToken}).end(function (error, result) {
|
||||
// use http admin origin so that it works with self-signed certs
|
||||
superagent
|
||||
.get(config.internalAdminOrigin() + '/api/v1/profile')
|
||||
.query({ access_token: req.sessionData.accessToken})
|
||||
.end(function (error, result) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
req.authenticated = false;
|
||||
@@ -85,7 +84,11 @@ function authenticate(req, res, next) {
|
||||
client_secret: req.sessionData.clientSecret
|
||||
};
|
||||
|
||||
superagent.post(config.adminOrigin() + '/api/v1/oauth/token').query(query).send(data).end(function (error, result) {
|
||||
// use http admin origin so that it works with self-signed certs
|
||||
superagent
|
||||
.post(config.internalAdminOrigin() + '/api/v1/oauth/token')
|
||||
.query(query).send(data)
|
||||
.end(function (error, result) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return res.send(500, 'Unable to contact the oauth server.');
|
||||
@@ -174,13 +177,12 @@ function initializeServer() {
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
function start(port, callback) {
|
||||
assert.strictEqual(typeof port, 'number');
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gHttpServer = initializeServer();
|
||||
|
||||
gHttpServer.listen(port, callback);
|
||||
gHttpServer.listen(config.get('oauthProxyPort'), callback);
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
|
||||
+3
-1
@@ -24,5 +24,7 @@ exports = module.exports = {
|
||||
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'),
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
|
||||
FAVICON_FILE: path.join(__dirname + '/../assets/favicon.ico')
|
||||
FAVICON_FILE: path.join(__dirname + '/../assets/favicon.ico'),
|
||||
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json')
|
||||
};
|
||||
|
||||
+6
-3
@@ -43,6 +43,7 @@ function removeInternalAppFields(app) {
|
||||
health: app.health,
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
oauthProxy: app.oauthProxy,
|
||||
lastBackupId: app.lastBackupId,
|
||||
manifest: app.manifest,
|
||||
portBindings: app.portBindings,
|
||||
@@ -114,14 +115,15 @@ function installApp(req, res, next) {
|
||||
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
|
||||
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
if (typeof data.accessRestriction !== 'string') return next(new HttpError(400, 'accessRestriction is required'));
|
||||
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
|
||||
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
|
||||
|
||||
// allow tests to provide an appId for testing
|
||||
var appId = (process.env.BOX_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
|
||||
|
||||
debug('Installing app id:%s storeid:%s loc:%s port:%j restrict:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.manifest);
|
||||
debug('Installing app id:%s storeid:%s loc:%s port:%j restrict:%s oauthproxy:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.oauthProxy, data.manifest);
|
||||
|
||||
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, function (error) {
|
||||
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, data.icon || null, function (error) {
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
@@ -150,10 +152,11 @@ function configureApp(req, res, next) {
|
||||
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
|
||||
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
if (typeof data.accessRestriction !== 'string') return next(new HttpError(400, 'accessRestriction is required'));
|
||||
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
|
||||
|
||||
debug('Configuring app id:%s location:%s bindings:%j', req.params.id, data.location, data.portBindings);
|
||||
|
||||
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, function (error) {
|
||||
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, function (error) {
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
|
||||
@@ -7,6 +7,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var cloudron = require('../cloudron.js'),
|
||||
CloudronError = require('../cloudron.js').CloudronError,
|
||||
debug = require('debug')('box:routes/internal'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
@@ -14,10 +15,12 @@ var cloudron = require('../cloudron.js'),
|
||||
function backup(req, res, next) {
|
||||
debug('trigger backup');
|
||||
|
||||
// note that cloudron.backup only waits for backup initiation and not for backup to complete
|
||||
// backup progress can be checked up ny polling the progress api call
|
||||
cloudron.backup(function (error) {
|
||||
if (error) debug('Internal route backup failed', error);
|
||||
});
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
// we always succeed to trigger a backup
|
||||
next(new HttpSuccess(202, {}));
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
+65
-69
@@ -148,6 +148,21 @@ session.ensureLoggedIn = function (redirectTo) {
|
||||
};
|
||||
};
|
||||
|
||||
function renderTemplate(res, template, data) {
|
||||
assert.strictEqual(typeof res, 'object');
|
||||
assert.strictEqual(typeof template, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// amend details which the header expects
|
||||
data.cloudronName = cloudronName || 'Cloudron';
|
||||
|
||||
res.render(template, data);
|
||||
});
|
||||
}
|
||||
|
||||
function sendErrorPageOrRedirect(req, res, message) {
|
||||
assert.strictEqual(typeof req, 'object');
|
||||
assert.strictEqual(typeof res, 'object');
|
||||
@@ -156,16 +171,19 @@ function sendErrorPageOrRedirect(req, res, message) {
|
||||
debug('sendErrorPageOrRedirect: returnTo "%s".', req.query.returnTo, message);
|
||||
|
||||
if (typeof req.query.returnTo !== 'string') {
|
||||
res.render('error', {
|
||||
renderTemplate(res, 'error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: message
|
||||
});
|
||||
} else {
|
||||
var u = url.parse(req.query.returnTo);
|
||||
if (!u.protocol || !u.host) return res.render('error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: 'Invalid request. returnTo query is not a valid URI. ' + message
|
||||
});
|
||||
if (!u.protocol || !u.host) {
|
||||
renderTemplate(res, 'error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: 'Invalid request. returnTo query is not a valid URI. ' + message
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.redirect(util.format('%s//%s', u.protocol, u.host));
|
||||
}
|
||||
@@ -178,7 +196,7 @@ function sendError(req, res, message) {
|
||||
assert.strictEqual(typeof res, 'object');
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
res.render('error', {
|
||||
renderTemplate(res, 'error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: message
|
||||
});
|
||||
@@ -191,47 +209,40 @@ function loginForm(req, res) {
|
||||
var u = url.parse(req.session.returnTo, true);
|
||||
if (!u.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid login request. No client_id provided.');
|
||||
|
||||
var cloudronName = '';
|
||||
|
||||
function render(applicationName, applicationLogo) {
|
||||
res.render('login', {
|
||||
renderTemplate(res, 'login', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
csrf: req.csrfToken(),
|
||||
cloudronName: cloudronName,
|
||||
applicationName: applicationName,
|
||||
applicationLogo: applicationLogo,
|
||||
error: req.query.error || null
|
||||
});
|
||||
}
|
||||
|
||||
settings.getCloudronName(function (error, name) {
|
||||
if (error) return sendError(req, res, 'Internal Error');
|
||||
clientdb.get(u.query.client_id, function (error, result) {
|
||||
if (error) return sendError(req, res, 'Unknown OAuth client');
|
||||
|
||||
cloudronName = name;
|
||||
// Handle our different types of oauth clients
|
||||
var appId = result.appId;
|
||||
if (appId === constants.ADMIN_CLIENT_ID) {
|
||||
return render(constants.ADMIN_NAME, '/api/v1/cloudron/avatar');
|
||||
} else if (appId === constants.TEST_CLIENT_ID) {
|
||||
return render(constants.TEST_NAME, '/api/v1/cloudron/avatar');
|
||||
} else if (appId.indexOf('external-') === 0) {
|
||||
return render('External Application', '/api/v1/cloudron/avatar');
|
||||
} else if (appId.indexOf('addon-oauth-') === 0) {
|
||||
appId = appId.slice('addon-oauth-'.length);
|
||||
} else if (appId.indexOf('addon-simpleauth-') === 0) {
|
||||
appId = appId.slice('addon-simpleauth-'.length);
|
||||
} else if (appId.indexOf('proxy-') === 0) {
|
||||
appId = appId.slice('proxy-'.length);
|
||||
}
|
||||
|
||||
clientdb.get(u.query.client_id, function (error, result) {
|
||||
if (error) return sendError(req, res, 'Unknown OAuth client');
|
||||
appdb.get(appId, function (error, result) {
|
||||
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
|
||||
|
||||
// Handle our different types of oauth clients
|
||||
var appId = result.appId;
|
||||
if (appId === constants.ADMIN_CLIENT_ID) {
|
||||
return render(constants.ADMIN_NAME, '/api/v1/cloudron/avatar');
|
||||
} else if (appId === constants.TEST_CLIENT_ID) {
|
||||
return render(constants.TEST_NAME, '/api/v1/cloudron/avatar');
|
||||
} else if (appId.indexOf('external-') === 0) {
|
||||
return render('External Application', '/api/v1/cloudron/avatar');
|
||||
} else if (appId.indexOf('addon-') === 0) {
|
||||
appId = appId.slice('addon-'.length);
|
||||
} else if (appId.indexOf('proxy-') === 0) {
|
||||
appId = appId.slice('proxy-'.length);
|
||||
}
|
||||
|
||||
appdb.get(appId, function (error, result) {
|
||||
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
|
||||
|
||||
var applicationName = result.location || config.fqdn();
|
||||
render(applicationName, '/api/v1/cloudron/avatar');
|
||||
});
|
||||
var applicationName = result.location || config.fqdn();
|
||||
render(applicationName, '/api/v1/apps/' + result.id + '/icon');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -259,7 +270,7 @@ function logout(req, res) {
|
||||
// Form to enter email address to send a password reset request mail
|
||||
// -> GET /api/v1/session/password/resetRequest.html
|
||||
function passwordResetRequestSite(req, res) {
|
||||
res.render('password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken() });
|
||||
renderTemplate(res, 'password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken() });
|
||||
}
|
||||
|
||||
// This route is used for above form submission
|
||||
@@ -283,7 +294,7 @@ function passwordResetRequest(req, res, next) {
|
||||
|
||||
// -> GET /api/v1/session/password/sent.html
|
||||
function passwordSentSite(req, res) {
|
||||
res.render('password_reset_sent', { adminOrigin: config.adminOrigin() });
|
||||
renderTemplate(res, 'password_reset_sent', { adminOrigin: config.adminOrigin() });
|
||||
}
|
||||
|
||||
// -> GET /api/v1/session/password/setup.html
|
||||
@@ -295,7 +306,12 @@ function passwordSetupSite(req, res, next) {
|
||||
user.getByResetToken(req.query.reset_token, function (error, user) {
|
||||
if (error) return next(new HttpError(401, 'Invalid reset_token'));
|
||||
|
||||
res.render('password_setup', { adminOrigin: config.adminOrigin(), user: user, csrf: req.csrfToken(), resetToken: req.query.reset_token });
|
||||
renderTemplate(res, 'password_setup', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
user: user,
|
||||
csrf: req.csrfToken(),
|
||||
resetToken: req.query.reset_token
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -308,7 +324,12 @@ function passwordResetSite(req, res, next) {
|
||||
user.getByResetToken(req.query.reset_token, function (error, user) {
|
||||
if (error) return next(new HttpError(401, 'Invalid reset_token'));
|
||||
|
||||
res.render('password_reset', { adminOrigin: config.adminOrigin(), user: user, csrf: req.csrfToken(), resetToken: req.query.reset_token });
|
||||
renderTemplate(res, 'password_reset', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
user: user,
|
||||
csrf: req.csrfToken(),
|
||||
resetToken: req.query.reset_token
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -343,7 +364,7 @@ var callback = [
|
||||
session.ensureLoggedIn('/api/v1/session/login'),
|
||||
function (req, res) {
|
||||
debug('callback: with callback server ' + req.query.redirectURI);
|
||||
res.render('callback', { adminOrigin: config.adminOrigin(), callbackServer: req.query.redirectURI });
|
||||
renderTemplate(res, 'callback', { adminOrigin: config.adminOrigin(), callbackServer: req.query.redirectURI });
|
||||
}
|
||||
];
|
||||
|
||||
@@ -400,35 +421,11 @@ var authorization = [
|
||||
callback(null, client, '/api/v1/session/callback?redirectURI=' + url.resolve(redirectOrigin, redirectPath));
|
||||
});
|
||||
}),
|
||||
// Until we have OAuth scopes, skip decision dialog
|
||||
// OAuth sopes skip START
|
||||
function (req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.oauth2, 'object');
|
||||
|
||||
var scopes = req.oauth2.client.scope ? req.oauth2.client.scope.split(',') : ['profile','roleUser'];
|
||||
|
||||
if (scopes.indexOf('roleAdmin') !== -1 && !req.user.admin) {
|
||||
return sendErrorPageOrRedirect(req, res, 'Admin capabilities required');
|
||||
}
|
||||
|
||||
req.body.transaction_id = req.oauth2.transactionID;
|
||||
next();
|
||||
},
|
||||
gServer.decision(function(req, done) {
|
||||
debug('decision: with scope', req.oauth2.req.scope);
|
||||
return done(null, { scope: req.oauth2.req.scope });
|
||||
// we do not have a decision dialog, no need to load the transaction
|
||||
gServer.decision({ loadTransaction: false }, function (req, done) {
|
||||
debug('decision: with scope', req.oauth2.client.scope);
|
||||
return done(null, { scope: req.oauth2.client.scope });
|
||||
})
|
||||
// OAuth sopes skip END
|
||||
// function (req, res) {
|
||||
// res.render('dialog', { transactionID: req.oauth2.transactionID, user: req.user, client: req.oauth2.client, csrf: req.csrfToken() });
|
||||
// }
|
||||
];
|
||||
|
||||
// this triggers the above grant middleware and handles the user's decision if he accepts the access
|
||||
var decision = [
|
||||
session.ensureLoggedIn('/api/v1/session/login'),
|
||||
gServer.decision()
|
||||
];
|
||||
|
||||
|
||||
@@ -509,7 +506,6 @@ exports = module.exports = {
|
||||
passwordSetupSite: passwordSetupSite,
|
||||
passwordReset: passwordReset,
|
||||
authorization: authorization,
|
||||
decision: decision,
|
||||
token: token,
|
||||
scope: scope,
|
||||
csrf: csrf
|
||||
|
||||
@@ -40,12 +40,17 @@ var appdb = require('../../appdb.js'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
// Test image information
|
||||
var TEST_IMAGE_REPO = 'cloudron/test';
|
||||
var TEST_IMAGE_TAG = '6.0.0';
|
||||
var TEST_IMAGE_ID = '7a53b21358cd7b014d29ee85f16ac535c37c11fb1f4c124197941236eb4d7c64';
|
||||
|
||||
var APP_STORE_ID = 'test', APP_ID;
|
||||
var APP_LOCATION = 'appslocation';
|
||||
var APP_LOCATION_2 = 'appslocationtwo';
|
||||
var APP_LOCATION_NEW = 'appslocationnew';
|
||||
var APP_MANIFEST = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
|
||||
APP_MANIFEST.dockerImage = 'girish/test:0.2.0';
|
||||
APP_MANIFEST.dockerImage = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='admin@me.com';
|
||||
var USERNAME_1 = 'user', PASSWORD_1 = 'password', EMAIL_1 ='user@me.com';
|
||||
var token = null; // authentication token
|
||||
@@ -152,7 +157,7 @@ function cleanup(done) {
|
||||
|
||||
function (callback) { setTimeout(callback, 2000); }, // give taskmanager tasks couple of seconds to finish
|
||||
|
||||
child_process.exec.bind(null, 'docker rm -f mysql; docker rm -f postgresql; docker rm -f mongodb')
|
||||
child_process.exec.bind(null, 'docker rm -f mysql; docker rm -f postgresql; docker rm -f mongodb; docker rm -f mail')
|
||||
], done);
|
||||
}
|
||||
|
||||
@@ -162,7 +167,7 @@ describe('App API', function () {
|
||||
|
||||
before(function (done) {
|
||||
dockerProxy = startDockerProxy(function interceptor(req, res) {
|
||||
if (req.method === 'DELETE' && req.url === '/images/c7ddfc8fb7cd8a14d4d70153a199ff0c6e9b709807aeec5a7b799d60618731d1?force=true&noprune=false') {
|
||||
if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE_ID + '?force=true&noprune=false') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return true;
|
||||
@@ -216,7 +221,7 @@ describe('App API', function () {
|
||||
it('app install fails - invalid location', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: '' })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('Hostname can only contain alphanumerics and hyphen');
|
||||
@@ -227,7 +232,7 @@ describe('App API', function () {
|
||||
it('app install fails - invalid location type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: '' })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('location is required');
|
||||
@@ -238,7 +243,7 @@ describe('App API', function () {
|
||||
it('app install fails - reserved admin location', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.ADMIN_LOCATION, accessRestriction: '' })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.ADMIN_LOCATION, accessRestriction: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql(constants.ADMIN_LOCATION + ' is reserved');
|
||||
@@ -249,7 +254,7 @@ describe('App API', function () {
|
||||
it('app install fails - reserved api location', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.API_LOCATION, accessRestriction: '' })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.API_LOCATION, accessRestriction: '', oauthProxy: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
|
||||
@@ -260,7 +265,7 @@ describe('App API', function () {
|
||||
it('app install fails - portBindings must be object', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: 23, accessRestriction: '' })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: 23, accessRestriction: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('portBindings must be an object');
|
||||
@@ -271,7 +276,7 @@ describe('App API', function () {
|
||||
it('app install fails - accessRestriction is required', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {} })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('accessRestriction is required');
|
||||
@@ -279,10 +284,21 @@ describe('App API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - oauthProxy is required', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: '' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('oauthProxy must be a boolean');
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails for non admin', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
@@ -294,7 +310,7 @@ describe('App API', function () {
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(402);
|
||||
expect(fake.isDone()).to.be.ok();
|
||||
@@ -307,7 +323,7 @@ describe('App API', function () {
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
expect(res.body.id).to.be.a('string');
|
||||
@@ -322,7 +338,7 @@ describe('App API', function () {
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(409);
|
||||
expect(fake.isDone()).to.be.ok();
|
||||
@@ -446,7 +462,7 @@ describe('App API', function () {
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION_2, portBindings: null, accessRestriction: '' })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION_2, portBindings: null, accessRestriction: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
expect(res.body.id).to.be.a('string');
|
||||
@@ -475,7 +491,7 @@ describe('App API', function () {
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: '' })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
expect(res.body.id).to.be.a('string');
|
||||
@@ -510,12 +526,12 @@ describe('App installation', function () {
|
||||
async.series([
|
||||
function (callback) {
|
||||
dockerProxy = startDockerProxy(function interceptor(req, res) {
|
||||
if (req.method === 'POST' && req.url === '/images/create?fromImage=girish%2Ftest&tag=0.2.0') {
|
||||
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
|
||||
imageCreated = true;
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return true;
|
||||
} else if (req.method === 'DELETE' && req.url === '/images/c7ddfc8fb7cd8a14d4d70153a199ff0c6e9b709807aeec5a7b799d60618731d1?force=true&noprune=false') {
|
||||
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE_ID + '?force=true&noprune=false') {
|
||||
imageDeleted = true;
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
@@ -586,7 +602,7 @@ describe('App installation', function () {
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
|
||||
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
expect(fake.isDone()).to.be.ok();
|
||||
@@ -618,9 +634,9 @@ describe('App installation', function () {
|
||||
expect(data.Config.Env).to.contain('WEBADMIN_ORIGIN=' + config.adminOrigin());
|
||||
expect(data.Config.Env).to.contain('API_ORIGIN=' + config.adminOrigin());
|
||||
expect(data.Config.Env).to.contain('CLOUDRON=1');
|
||||
clientdb.getByAppId('addon-' + appResult.id, function (error, client) {
|
||||
clientdb.getByAppId('addon-oauth-' + appResult.id, function (error, client) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(client.id.length).to.be(46); // cid-addon- + 32 hex chars (128 bits) + 4 hyphens
|
||||
expect(client.id.length).to.be(52); // cid-addon-oauth- + 32 hex chars (128 bits) + 4 hyphens
|
||||
expect(client.clientSecret.length).to.be(64); // 32 hex chars (256 bits)
|
||||
expect(data.Config.Env).to.contain('OAUTH_CLIENT_ID=' + client.id);
|
||||
expect(data.Config.Env).to.contain('OAUTH_CLIENT_SECRET=' + client.clientSecret);
|
||||
@@ -659,7 +675,14 @@ describe('App installation', function () {
|
||||
it('installation - running container has volume mounted', function (done) {
|
||||
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
|
||||
|
||||
// support newer docker versions
|
||||
if (data.Volumes) {
|
||||
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
|
||||
} else {
|
||||
expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Source).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -735,7 +758,7 @@ describe('App installation', function () {
|
||||
child_process.exec('docker exec ' + appContainer.id + ' ' + cmd, { timeout: 5000 }, function (error, stdout, stderr) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(stdout.length).to.be(0);
|
||||
expect(stderr.length).to.be(0);
|
||||
// expect(stderr.length).to.be(0); // "Warning: Using a password on the command line interface can be insecure."
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -959,12 +982,12 @@ describe('App installation - port bindings', function () {
|
||||
async.series([
|
||||
function (callback) {
|
||||
dockerProxy = startDockerProxy(function interceptor(req, res) {
|
||||
if (req.method === 'POST' && req.url === '/images/create?fromImage=girish%2Ftest&tag=0.2.0') {
|
||||
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
|
||||
imageCreated = true;
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return true;
|
||||
} else if (req.method === 'DELETE' && req.url === '/images/c7ddfc8fb7cd8a14d4d70153a199ff0c6e9b709807aeec5a7b799d60618731d1?force=true&noprune=false') {
|
||||
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE_ID + '?force=true&noprune=false') {
|
||||
imageDeleted = true;
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
@@ -1034,7 +1057,7 @@ describe('App installation - port bindings', function () {
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: '' })
|
||||
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
expect(fake.isDone()).to.be.ok();
|
||||
@@ -1114,7 +1137,14 @@ describe('App installation - port bindings', function () {
|
||||
it('installation - running container has volume mounted', function (done) {
|
||||
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
|
||||
|
||||
// support newer docker versions
|
||||
if (data.Volumes) {
|
||||
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
|
||||
} else {
|
||||
expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Source).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -1188,7 +1218,7 @@ describe('App installation - port bindings', function () {
|
||||
it('cannot reconfigure app with missing location', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: APP_ID, password: PASSWORD, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin' })
|
||||
.send({ appId: APP_ID, password: PASSWORD, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin', oauthProxy: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
@@ -1198,7 +1228,17 @@ describe('App installation - port bindings', function () {
|
||||
it('cannot reconfigure app with missing accessRestriction', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 } })
|
||||
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot reconfigure app with missing oauthProxy', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: '' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
@@ -1208,7 +1248,7 @@ describe('App installation - port bindings', function () {
|
||||
it('non admin cannot reconfigure app', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin' })
|
||||
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin', oauthProxy: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done();
|
||||
@@ -1218,7 +1258,7 @@ describe('App installation - port bindings', function () {
|
||||
it('can reconfigure app', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin' })
|
||||
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: '', oauthProxy: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
checkConfigureStatus(0, done);
|
||||
|
||||
@@ -51,7 +51,7 @@ function setup(done) {
|
||||
|
||||
function addApp(callback) {
|
||||
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, callback);
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, false /* oauthProxy */, callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
@@ -591,8 +591,8 @@ describe('Clients', function () {
|
||||
email: 'some@email.com',
|
||||
admin: true,
|
||||
salt: 'somesalt',
|
||||
createdAt: (new Date()).toUTCString(),
|
||||
modifiedAt: (new Date()).toUTCString(),
|
||||
createdAt: (new Date()).toISOString(),
|
||||
modifiedAt: (new Date()).toISOString(),
|
||||
resetToken: hat(256)
|
||||
};
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ function setup(done) {
|
||||
|
||||
function addApp(callback) {
|
||||
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok' };
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, callback);
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, false /* oauthProxy */, callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var clientdb = require('../../clientdb.js'),
|
||||
async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
request = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
simpleauth = require('../../simpleauth.js'),
|
||||
nock = require('nock'),
|
||||
userdb = require('../../userdb.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
var SIMPLE_AUTH_ORIGIN = 'http://localhost:' + config.get('simpleAuthPort');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var CLIENT = {
|
||||
id: 'someclientid',
|
||||
appId: 'someappid',
|
||||
clientSecret: 'someclientsecret',
|
||||
redirectURI: '',
|
||||
scope: 'user,profile'
|
||||
};
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
simpleauth.start.bind(simpleauth),
|
||||
|
||||
userdb._clear,
|
||||
|
||||
function createAdmin(callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function addClient(callback) {
|
||||
clientdb.add(CLIENT.id, CLIENT.appId, CLIENT.clientSecret, CLIENT.redirectURI, CLIENT.scope, callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('SimpleAuth API', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
describe('login', function () {
|
||||
it('cannot login without clientId', function (done) {
|
||||
var body = {};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot login without username', function (done) {
|
||||
var body = {
|
||||
clientId: 'someclientid'
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot login without password', function (done) {
|
||||
var body = {
|
||||
clientId: 'someclientid',
|
||||
username: USERNAME
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot login with unkown clientId', function (done) {
|
||||
var body = {
|
||||
clientId: CLIENT.id+CLIENT.id,
|
||||
username: USERNAME,
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot login with unkown user', function (done) {
|
||||
var body = {
|
||||
clientId: CLIENT.id,
|
||||
username: USERNAME+USERNAME,
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot login with empty password', function (done) {
|
||||
var body = {
|
||||
clientId: CLIENT.id,
|
||||
username: USERNAME,
|
||||
password: ''
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot login with wrgon password', function (done) {
|
||||
var body = {
|
||||
clientId: CLIENT.id,
|
||||
username: USERNAME,
|
||||
password: PASSWORD+PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var body = {
|
||||
clientId: CLIENT.id,
|
||||
username: USERNAME,
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.accessToken).to.be.a('string');
|
||||
expect(result.body.user).to.be.an('object');
|
||||
expect(result.body.user.id).to.be.a('string');
|
||||
expect(result.body.user.username).to.be.a('string');
|
||||
expect(result.body.user.email).to.be.a('string');
|
||||
expect(result.body.user.admin).to.be.a('boolean');
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: result.body.accessToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.body).to.be.an('object');
|
||||
expect(result.body.username).to.eql(USERNAME);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', function () {
|
||||
var accessToken;
|
||||
|
||||
before(function (done) {
|
||||
var body = {
|
||||
clientId: CLIENT.id,
|
||||
username: USERNAME,
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
accessToken = result.body.accessToken;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without access_token', function (done) {
|
||||
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with unkonwn access_token', function (done) {
|
||||
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
|
||||
.query({ access_token: accessToken+accessToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
|
||||
.query({ access_token: accessToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: accessToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@ root_password=secret
|
||||
start_postgresql() {
|
||||
postgresql_vars="POSTGRESQL_ROOT_PASSWORD=${root_password}; POSTGRESQL_ROOT_HOST=172.17.0.0/255.255.0.0"
|
||||
|
||||
if which boot2docker >/dev/null; then
|
||||
if which boot2docker >/dev/null 2>&1; then
|
||||
boot2docker ssh "sudo rm -rf /tmp/postgresql_vars.sh"
|
||||
boot2docker ssh "echo \"${postgresql_vars}\" > /tmp/postgresql_vars.sh"
|
||||
else
|
||||
@@ -24,13 +24,15 @@ start_postgresql() {
|
||||
|
||||
docker rm -f postgresql 2>/dev/null 1>&2 || true
|
||||
|
||||
docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" -v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh "${POSTGRESQL_IMAGE}" >/dev/null
|
||||
docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" \
|
||||
--read-only -v /tmp -v /run -v /var/log \
|
||||
-v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh "${POSTGRESQL_IMAGE}" >/dev/null
|
||||
}
|
||||
|
||||
start_mysql() {
|
||||
local mysql_vars="MYSQL_ROOT_PASSWORD=${root_password}; MYSQL_ROOT_HOST=172.17.0.0/255.255.0.0"
|
||||
|
||||
if which boot2docker >/dev/null; then
|
||||
if which boot2docker >/dev/null 2>&1; then
|
||||
boot2docker ssh "sudo rm -rf /tmp/mysql_vars.sh"
|
||||
boot2docker ssh "echo \"${mysql_vars}\" > /tmp/mysql_vars.sh"
|
||||
else
|
||||
@@ -40,13 +42,15 @@ start_mysql() {
|
||||
|
||||
docker rm -f mysql 2>/dev/null 1>&2 || true
|
||||
|
||||
docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" -v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh "${MYSQL_IMAGE}" >/dev/null
|
||||
docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" \
|
||||
--read-only -v /tmp -v /run -v /var/log \
|
||||
-v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh "${MYSQL_IMAGE}" >/dev/null
|
||||
}
|
||||
|
||||
start_mongodb() {
|
||||
local mongodb_vars="MONGODB_ROOT_PASSWORD=${root_password}"
|
||||
|
||||
if which boot2docker >/dev/null; then
|
||||
if which boot2docker >/dev/null 2>&1; then
|
||||
boot2docker ssh "sudo rm -rf /tmp/mongodb_vars.sh"
|
||||
boot2docker ssh "echo \"${mongodb_vars}\" > /tmp/mongodb_vars.sh"
|
||||
else
|
||||
@@ -56,16 +60,29 @@ start_mongodb() {
|
||||
|
||||
docker rm -f mongodb 2>/dev/null 1>&2 || true
|
||||
|
||||
docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" -v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh "${MONGODB_IMAGE}" >/dev/null
|
||||
docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" \
|
||||
--read-only -v /tmp -v /run -v /var/log \
|
||||
-v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh "${MONGODB_IMAGE}" >/dev/null
|
||||
}
|
||||
|
||||
start_mail() {
|
||||
local mongodb_vars="MONGODB_ROOT_PASSWORD=${root_password}"
|
||||
|
||||
docker rm -f mail 2>/dev/null 1>&2 || true
|
||||
|
||||
docker run -dP --name=mail -e DOMAIN_NAME="localhost" \
|
||||
--read-only -v /tmp -v /run -v /var/log \
|
||||
-v /tmp/maildata:/app/data "${MAIL_IMAGE}" >/dev/null
|
||||
}
|
||||
|
||||
start_mysql
|
||||
start_postgresql
|
||||
start_mongodb
|
||||
start_mail
|
||||
|
||||
echo -n "Waiting for addons to start"
|
||||
for i in {1..10}; do
|
||||
echo -n "."
|
||||
for i in {1..20}; do
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
echo ""
|
||||
|
||||
+4
-8
@@ -43,11 +43,7 @@ function initializeExpressSync() {
|
||||
app.set('view options', { layout: true, debug: true });
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
if (process.env.BOX_ENV === 'test') {
|
||||
app.use(express.static(path.join(__dirname, '/../webadmin')));
|
||||
} else {
|
||||
app.use(middleware.morgan('dev', { immediate: false }));
|
||||
}
|
||||
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
|
||||
|
||||
var router = new express.Router();
|
||||
router.del = router.delete; // amend router.del for readability further on
|
||||
@@ -130,7 +126,6 @@ function initializeExpressSync() {
|
||||
|
||||
// oauth2 routes
|
||||
router.get ('/api/v1/oauth/dialog/authorize', routes.oauth2.authorization);
|
||||
router.post('/api/v1/oauth/dialog/authorize/decision', csrf, routes.oauth2.decision);
|
||||
router.post('/api/v1/oauth/token', routes.oauth2.token);
|
||||
router.get ('/api/v1/oauth/clients', settingsScope, routes.clients.getAllByUserId);
|
||||
router.post('/api/v1/oauth/clients', routes.developer.enabled, settingsScope, routes.clients.add);
|
||||
@@ -144,7 +139,7 @@ function initializeExpressSync() {
|
||||
// app routes
|
||||
router.get ('/api/v1/apps', appsScope, routes.apps.getApps);
|
||||
router.get ('/api/v1/apps/:id', appsScope, routes.apps.getApp);
|
||||
router.get ('/api/v1/apps/:id/icon', appsScope, routes.apps.getAppIcon);
|
||||
router.get ('/api/v1/apps/:id/icon', routes.apps.getAppIcon);
|
||||
|
||||
router.post('/api/v1/apps/install', appsScope, routes.user.requireAdmin, routes.apps.installApp);
|
||||
router.post('/api/v1/apps/:id/uninstall', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.uninstallApp);
|
||||
@@ -199,6 +194,7 @@ function initializeExpressSync() {
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
// provides hooks for the 'installer'
|
||||
function initializeInternalExpressSync() {
|
||||
var app = express();
|
||||
var httpServer = http.createServer(app);
|
||||
@@ -209,7 +205,7 @@ function initializeInternalExpressSync() {
|
||||
var json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
|
||||
urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded
|
||||
|
||||
app.use(middleware.morgan('dev', { immediate: false }));
|
||||
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box Internal :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
|
||||
|
||||
var router = new express.Router();
|
||||
router.del = router.delete; // amend router.del for readability further on
|
||||
|
||||
+1
-4
@@ -42,12 +42,9 @@ var assert = require('assert'),
|
||||
_ = require('underscore');
|
||||
|
||||
var gDefaults = (function () {
|
||||
var tz = safe.fs.readFileSync('/etc/timezone', 'utf8');
|
||||
tz = tz ? tz.trim() : 'America/Los_Angeles';
|
||||
|
||||
var result = { };
|
||||
result[exports.AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
|
||||
result[exports.TIME_ZONE_KEY] = tz;
|
||||
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
|
||||
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
|
||||
result[exports.DEVELOPER_MODE_KEY] = false;
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:simpleauth'),
|
||||
user = require('./user.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:proxy'),
|
||||
middleware = require('./middleware'),
|
||||
express = require('express'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
UserError = require('./user.js').UserError,
|
||||
http = require('http');
|
||||
|
||||
var gHttpServer = null;
|
||||
|
||||
function loginLogic(clientId, username, password, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('login: client %s and user %s', clientId, username);
|
||||
|
||||
clients.get(clientId, function (error, clientObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
user.verify(username, password, function (error, userObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var accessToken = tokendb.generateToken();
|
||||
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
|
||||
|
||||
tokendb.add(accessToken, tokendb.PREFIX_USER + userObject.id, clientId, expires, clientObject.scope, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('login: new access token for client %s and user %s: %s', clientId, username, accessToken);
|
||||
|
||||
callback(null, { accessToken: accessToken, user: userObject });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function logoutLogic(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('logout: %s', accessToken);
|
||||
|
||||
tokendb.del(accessToken, function (error) {
|
||||
if (error) return callback(error);
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function login(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.clientId !== 'string') return next(new HttpError(400, 'clientId is required'));
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username is required'));
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
|
||||
|
||||
loginLogic(req.body.clientId, req.body.username, req.body.password, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(401, 'Unknown client'));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(401, 'Forbidden'));
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(401, 'Forbidden'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
var tmp = {
|
||||
accessToken: result.accessToken,
|
||||
user: {
|
||||
id: result.user.id,
|
||||
username: result.user.username,
|
||||
email: result.user.email,
|
||||
admin: !!result.user.admin
|
||||
}
|
||||
};
|
||||
|
||||
next(new HttpSuccess(200, tmp));
|
||||
});
|
||||
}
|
||||
|
||||
function logout(req, res, next) {
|
||||
assert.strictEqual(typeof req.query, 'object');
|
||||
|
||||
if (typeof req.query.access_token !== 'string') return next(new HttpError(400, 'access_token in query required'));
|
||||
|
||||
logoutLogic(req.query.access_token, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(401, 'Forbidden'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function initializeExpressSync() {
|
||||
var app = express();
|
||||
var httpServer = http.createServer(app);
|
||||
|
||||
httpServer.on('error', console.error);
|
||||
|
||||
var json = middleware.json({ strict: true, limit: '100kb' });
|
||||
var router = new express.Router();
|
||||
|
||||
// basic auth
|
||||
router.post('/api/v1/login', login);
|
||||
router.get ('/api/v1/logout', logout);
|
||||
|
||||
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('SimpleAuth :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
|
||||
|
||||
app
|
||||
.use(middleware.timeout(10000))
|
||||
.use(json)
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gHttpServer = initializeExpressSync();
|
||||
gHttpServer.listen(config.get('simpleAuthPort'), '0.0.0.0', callback);
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gHttpServer.close(callback);
|
||||
}
|
||||
@@ -80,8 +80,6 @@ function status(changeId, callback) {
|
||||
assert.strictEqual(typeof changeId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('status: ', changeId);
|
||||
|
||||
api().getChangeStatus(changeId, function (error, status) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
|
||||
callback(null, status === 'INSYNC' ? 'done' : 'pending');
|
||||
|
||||
@@ -48,6 +48,8 @@ function uninitialize(callback) {
|
||||
stopAppTask(appId);
|
||||
}
|
||||
|
||||
locker.removeListener('unlocked', startNextTask);
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,14 +36,15 @@ describe('Apps', function () {
|
||||
containerId: null,
|
||||
portBindings: { PORT: 5678 },
|
||||
healthy: null,
|
||||
accessRestriction: ''
|
||||
accessRestriction: '',
|
||||
oauthProxy: false
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear,
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction)
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy)
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ var MANIFEST = {
|
||||
"contactEmail": "support@cloudron.io",
|
||||
"version": "0.1.0",
|
||||
"manifestVersion": 1,
|
||||
"dockerImage": "girish/test:0.2.0",
|
||||
"dockerImage": "cloudron/test:2.0.0",
|
||||
"healthCheckPath": "/",
|
||||
"httpPort": 7777,
|
||||
"tcpPorts": {
|
||||
@@ -58,6 +58,7 @@ var APP = {
|
||||
httpPort: 4567,
|
||||
portBindings: null,
|
||||
accessRestriction: '',
|
||||
oauthProxy: false,
|
||||
dnsRecordId: 'someDnsRecordId'
|
||||
};
|
||||
|
||||
@@ -81,7 +82,7 @@ describe('apptask', function () {
|
||||
config.set('version', '0.5.0');
|
||||
database.initialize(function (error) {
|
||||
expect(error).to.be(null);
|
||||
appdb.add(APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, done);
|
||||
appdb.add(APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.oauthProxy, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,21 +137,21 @@ describe('apptask', function () {
|
||||
});
|
||||
|
||||
it('allocate OAuth credentials', function (done) {
|
||||
addons._allocateOAuthCredentials(APP, function (error) {
|
||||
addons._setupOauth(APP, {}, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('remove OAuth credentials', function (done) {
|
||||
addons._removeOAuthCredentials(APP, function (error) {
|
||||
addons._teardownOauth(APP, {}, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('remove OAuth credentials twice succeeds', function (done) {
|
||||
addons._removeOAuthCredentials(APP, function (error) {
|
||||
addons._teardownOauth(APP, {}, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -34,8 +34,8 @@ for script in "${scripts[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
if ! docker inspect girish/test:0.2.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull girish/test:0.2.0 for tests to run"
|
||||
if ! docker inspect cloudron/test:3.0.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull cloudron/test:3.0.0 for tests to run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -479,6 +479,7 @@ describe('database', function () {
|
||||
portBindings: { port: 5678 },
|
||||
health: null,
|
||||
accessRestriction: '',
|
||||
oauthProxy: false,
|
||||
lastBackupId: null,
|
||||
lastBackupConfig: null,
|
||||
oldConfig: null
|
||||
@@ -497,6 +498,7 @@ describe('database', function () {
|
||||
portBindings: { },
|
||||
health: null,
|
||||
accessRestriction: 'roleAdmin',
|
||||
oauthProxy: true,
|
||||
lastBackupId: null,
|
||||
lastBackupConfig: null,
|
||||
oldConfig: null
|
||||
@@ -516,7 +518,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('add succeeds', function (done) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, function (error) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
@@ -540,7 +542,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('add of same app fails', function (done) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, function (error) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, APP_0.oauthProxy, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
|
||||
done();
|
||||
@@ -569,10 +571,20 @@ describe('database', function () {
|
||||
APP_0.installationState = 'some-other-status';
|
||||
APP_0.location = 'some-other-location';
|
||||
APP_0.manifest.version = '0.2';
|
||||
APP_0.accessRestriction = true;
|
||||
APP_0.accessRestriction = '';
|
||||
APP_0.oauthProxy = true;
|
||||
APP_0.httpPort = 1337;
|
||||
|
||||
appdb.update(APP_0.id, { installationState: APP_0.installationState, location: APP_0.location, manifest: APP_0.manifest, accessRestriction: APP_0.accessRestriction, httpPort: APP_0.httpPort }, function (error) {
|
||||
var data = {
|
||||
installationState: APP_0.installationState,
|
||||
location: APP_0.location,
|
||||
manifest: APP_0.manifest,
|
||||
accessRestriction: APP_0.accessRestriction,
|
||||
oauthProxy: APP_0.oauthProxy,
|
||||
httpPort: APP_0.httpPort
|
||||
};
|
||||
|
||||
appdb.update(APP_0.id, data, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
appdb.get(APP_0.id, function (error, result) {
|
||||
@@ -602,7 +614,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('add second app succeeds', function (done) {
|
||||
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, function (error) {
|
||||
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, APP_0.oauthProxy, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
require('supererror', { splatchError: true});
|
||||
|
||||
var database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
EventEmitter = require('events').EventEmitter,
|
||||
|
||||
+32
-6
@@ -14,14 +14,23 @@ var apps = require('./apps.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:updatechecker'),
|
||||
mailer = require('./mailer.js'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
var gAppUpdateInfo = { }, // id -> update info { creationDate, manifest }
|
||||
gBoxUpdateInfo = null,
|
||||
gMailedUser = { };
|
||||
gBoxUpdateInfo = null;
|
||||
|
||||
function loadState() {
|
||||
var state = safe.JSON.parse(safe.fs.readFileSync(paths.UPDATE_CHECKER_FILE, 'utf8'));
|
||||
return state || { };
|
||||
}
|
||||
|
||||
function saveState(mailedUser) {
|
||||
safe.fs.writeFileSync(paths.UPDATE_CHECKER_FILE, JSON.stringify(mailedUser, null, 4), 'utf8');
|
||||
}
|
||||
|
||||
function getUpdateInfo() {
|
||||
return {
|
||||
@@ -72,6 +81,9 @@ function getAppUpdates(callback) {
|
||||
function getBoxUpdates(callback) {
|
||||
var currentVersion = config.version();
|
||||
|
||||
// do not crash if boxVersionsUrl is not set
|
||||
if (!config.get('boxVersionsUrl')) return callback(null, null);
|
||||
|
||||
superagent
|
||||
.get(config.get('boxVersionsUrl'))
|
||||
.timeout(10 * 1000)
|
||||
@@ -116,13 +128,21 @@ function getBoxUpdates(callback) {
|
||||
function checkAppUpdates() {
|
||||
debug('Checking App Updates');
|
||||
|
||||
var oldState = loadState();
|
||||
var newState = { box: oldState.box }; // creaee new state so that old app ids are removed
|
||||
|
||||
getAppUpdates(function (error, result) {
|
||||
if (error) debug('Error checking app updates: ', error);
|
||||
|
||||
gAppUpdateInfo = error ? {} : result;
|
||||
|
||||
async.eachSeries(Object.keys(gAppUpdateInfo), function iterator(id, iteratorDone) {
|
||||
if (gMailedUser[id]) return iteratorDone();
|
||||
newState[id] = gAppUpdateInfo[id].manifest.version;
|
||||
|
||||
if (oldState[id] === gAppUpdateInfo[id].manifest.version) {
|
||||
debug('Skipping notification of app update %s since user was already notified', id);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
apps.get(id, function (error, app) {
|
||||
if (error) {
|
||||
@@ -131,9 +151,10 @@ function checkAppUpdates() {
|
||||
}
|
||||
|
||||
mailer.appUpdateAvailable(app, gAppUpdateInfo[id]);
|
||||
gMailedUser[id] = true;
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
saveState(newState);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -141,14 +162,19 @@ function checkAppUpdates() {
|
||||
function checkBoxUpdates() {
|
||||
debug('Checking Box Updates');
|
||||
|
||||
var state = loadState();
|
||||
|
||||
getBoxUpdates(function (error, result) {
|
||||
if (error) debug('Error checking box updates: ', error);
|
||||
|
||||
gBoxUpdateInfo = error ? null : result;
|
||||
|
||||
if (gBoxUpdateInfo && !gMailedUser['box']) {
|
||||
if (gBoxUpdateInfo && state.box !== gBoxUpdateInfo.version) {
|
||||
mailer.boxUpdateAvailable(gBoxUpdateInfo.version, gBoxUpdateInfo.changelog);
|
||||
gMailedUser['box'] = true;
|
||||
state.box = gBoxUpdateInfo.version;
|
||||
saveState(state);
|
||||
} else {
|
||||
debug('Skipping notification of box update as user was already notified');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+2
-2
@@ -134,7 +134,7 @@ function createUser(username, password, email, admin, invitor, callback) {
|
||||
crypto.pbkdf2(password, salt, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
var now = (new Date()).toUTCString();
|
||||
var now = (new Date()).toISOString();
|
||||
var user = {
|
||||
id: username,
|
||||
username: username,
|
||||
@@ -331,7 +331,7 @@ function setPassword(userId, newPassword, callback) {
|
||||
crypto.pbkdf2(newPassword, saltBuffer, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
user.modifiedAt = (new Date()).toUTCString();
|
||||
user.modifiedAt = (new Date()).toISOString();
|
||||
user.password = new Buffer(derivedKey, 'binary').toString('hex');
|
||||
user.resetToken = '';
|
||||
|
||||
|
||||
@@ -75,9 +75,7 @@
|
||||
</div>
|
||||
<div ng-show="installedApps | readyToUpdate">
|
||||
<b ng-show="config.update.box.upgrade" class="text-danger">
|
||||
The update is a base system upgrade.<br/>
|
||||
This will cause some application downtime!<br/>
|
||||
<br/>
|
||||
This update upgrades the base system and will cause some application downtime.<br/>
|
||||
</b>
|
||||
<p>New version: <b>{{config.update.box.version}}</b></p>
|
||||
<p>Recent Changes:</p>
|
||||
|
||||
@@ -215,7 +215,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
|
||||
Client.prototype.installApp = function (id, manifest, title, config, callback) {
|
||||
var that = this;
|
||||
var data = { appStoreId: id, manifest: manifest, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction };
|
||||
var data = { appStoreId: id, manifest: manifest, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction, oauthProxy: config.oauthProxy };
|
||||
$http.post(client.apiOrigin + '/api/v1/apps/install', data).success(function (data, status) {
|
||||
if (status !== 202 || typeof data !== 'object') return defaultErrorHandler(callback);
|
||||
|
||||
@@ -249,7 +249,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
};
|
||||
|
||||
Client.prototype.configureApp = function (id, password, config, callback) {
|
||||
var data = { appId: id, password: password, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction };
|
||||
var data = { appId: id, password: password, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction, oauthProxy: config.oauthProxy };
|
||||
$http.post(client.apiOrigin + '/api/v1/apps/' + id + '/configure', data).success(function (data, status) {
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
@@ -610,12 +610,15 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
this._userInfo = {};
|
||||
|
||||
var callbackURL = window.location.protocol + '//' + window.location.host + '/login_callback.html';
|
||||
var scope = 'root,profile,apps,roleAdmin';
|
||||
var scope = 'root,profile,apps,roleUser';
|
||||
|
||||
// generate a state id to protect agains csrf
|
||||
var state = Math.floor((1 + Math.random()) * 0x1000000000000).toString(16).substring(1);
|
||||
window.localStorage.oauth2State = state;
|
||||
|
||||
// stash for further use in login_callback
|
||||
window.localStorage.returnTo = '/' + window.location.hash;
|
||||
|
||||
window.location.href = this.apiOrigin + '/api/v1/oauth/dialog/authorize?response_type=token&client_id=' + this._clientId + '&redirect_uri=' + callbackURL + '&scope=' + scope + '&state=' + state;
|
||||
};
|
||||
|
||||
|
||||
@@ -143,16 +143,6 @@ app.filter('applicationLink', function() {
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('accessRestrictionLabel', function() {
|
||||
return function (input) {
|
||||
if (input === '') return 'public';
|
||||
if (input === 'roleUser') return 'private';
|
||||
if (input === 'roleAdmin') return 'private (Admins only)';
|
||||
|
||||
return input;
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyHref', function () {
|
||||
return function (input) {
|
||||
if (!input) return input;
|
||||
|
||||
@@ -19,7 +19,11 @@
|
||||
// clear oauth2 state
|
||||
delete window.localStorage.oauth2State;
|
||||
|
||||
window.location.href = '/';
|
||||
var returnTo = window.localStorage.returnTo;
|
||||
delete window.localStorage.returnTo;
|
||||
|
||||
if (returnTo) window.location.href = returnTo;
|
||||
else window.location.href = '/';
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
+6
-18
@@ -120,7 +120,6 @@ html {
|
||||
.grid-item {
|
||||
padding: 10px;
|
||||
min-width: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.grid-item:hover .grid-item-bottom {
|
||||
@@ -175,6 +174,12 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
.app-update-badge {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Appstore view
|
||||
// ----------------------------
|
||||
@@ -354,23 +359,6 @@ html {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.app-update-badge {
|
||||
font-size: $font-size-h4;
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
width: $font-size-h4 + 6px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.app-update-badge:hover {
|
||||
width: inherit;
|
||||
background-color: #5CB85C;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #5CB85C;
|
||||
}
|
||||
|
||||
@@ -38,10 +38,16 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="accessRestriction">Website Visibility</label>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appConfigure.oauthProxy"> Cloudron users only
|
||||
</label>
|
||||
</div>
|
||||
<!-- <label class="control-label" for="accessRestriction">Website Visibility</label>
|
||||
<select class="form-control" id="accessRestriction" ng-model="appConfigure.accessRestriction">
|
||||
<option value="">Visible to all</option>
|
||||
<option value="roleUser">Visible only to Cloudron users</option>
|
||||
</select>
|
||||
</select> -->
|
||||
</div>
|
||||
<a ng-show="!!appConfigure.app.manifest.configurePath" ng-href="https://{{ appConfigure.app.location }}{{ !appConfigure.app.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}/{{ appConfigure.app.manifest.configurePath }}" target="_blank">Application Specific Settings</a>
|
||||
<br/>
|
||||
@@ -55,10 +61,6 @@
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" style="float: left;" ng-click="startApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'stopped' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-play"></i> Start</button>
|
||||
<button type="button" class="btn btn-default" style="float: left;" ng-show="appConfigure.app.runState !== 'stopped' && appConfigure.app.runState !== 'running' || appConfigure.runStateBusy && !(appConfigure.app | installationActive)" disabled ><i class="fa fa-refresh fa-spin"></i></button>
|
||||
<button type="button" class="btn btn-default" style="float: left;" ng-click="stopApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'running' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-pause"></i> Stop</button>
|
||||
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
|
||||
</div>
|
||||
@@ -209,8 +211,7 @@
|
||||
<div class="text-muted" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
|
||||
{{ app | installationStateLabel }}
|
||||
</div>
|
||||
<br ng-hide="app | installationActive"/>
|
||||
<div ng-show="app | installationActive">
|
||||
<div ng-style="{ 'visibility': (app | installationActive) ? 'visible' : 'hidden' }">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
|
||||
</div>
|
||||
@@ -244,26 +245,27 @@
|
||||
</div>
|
||||
<div class="grid-item-bottom" ng-show="user.admin">
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<a href="" ng-click="showUninstall(app)"><i class="fa fa-remove scale"></i></a>
|
||||
<a href="" ng-click="showUninstall(app)" title="Uninstall App"><i class="fa fa-remove scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="(app | installError) === true">
|
||||
<a href="" ng-click="showRestore(app)"><i class="fa fa-undo scale"></i></a>
|
||||
<a href="" ng-click="showRestore(app)" title="Restore App"><i class="fa fa-undo scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="(app | installSuccess) == true">
|
||||
<a href="" ng-click="showConfigure(app)"><i class="fa fa-wrench scale"></i></a>
|
||||
</div>
|
||||
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<div ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
||||
<a href="" ng-click="showUpdate(app)"><i class="fa fa-arrow-up text-success scale"></i></a>
|
||||
<a href="" ng-click="showConfigure(app)" title="Configure App"><i class="fa fa-wrench scale"></i></a>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<div class="app-update-badge" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
||||
<a href="" ng-click="showUpdate(app)" title="Update Available"><i class="fa fa-asterisk fa-2x text-success scale"></i></a>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,8 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
portBindings: {},
|
||||
portBindingsEnabled: {},
|
||||
portBindingsInfo: {},
|
||||
accessRestriction: ''
|
||||
accessRestriction: '',
|
||||
oauthProxy: false
|
||||
};
|
||||
|
||||
$scope.appUninstall = {
|
||||
@@ -53,6 +54,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.password = '';
|
||||
$scope.appConfigure.portBindings = {};
|
||||
$scope.appConfigure.accessRestriction = '';
|
||||
$scope.appConfigure.oauthProxy = false;
|
||||
|
||||
$scope.appConfigureForm.$setPristine();
|
||||
$scope.appConfigureForm.$setUntouched();
|
||||
@@ -90,6 +92,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.app = app;
|
||||
$scope.appConfigure.location = app.location;
|
||||
$scope.appConfigure.accessRestriction = app.accessRestriction;
|
||||
$scope.appConfigure.oauthProxy = app.oauthProxy;
|
||||
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope.appConfigure.portBindings = {}; // This is the actual model holding the env:port pair
|
||||
$scope.appConfigure.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
@@ -122,7 +125,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
}
|
||||
}
|
||||
|
||||
Client.configureApp($scope.appConfigure.app.id, $scope.appConfigure.password, { location: $scope.appConfigure.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appConfigure.accessRestriction }, function (error) {
|
||||
Client.configureApp($scope.appConfigure.app.id, $scope.appConfigure.password, { location: $scope.appConfigure.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appConfigure.accessRestriction, oauthProxy: $scope.appConfigure.oauthProxy }, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
|
||||
$scope.appConfigure.error.port = error.message;
|
||||
@@ -313,22 +316,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
});
|
||||
};
|
||||
|
||||
$scope.startApp = function (app) {
|
||||
$scope.appConfigure.runStateBusy = true;
|
||||
app.runState = 'pending_start'; // we assume we will end up there
|
||||
Client.startApp(app.id, function () {
|
||||
$scope.appConfigure.runStateBusy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.stopApp = function (app) {
|
||||
$scope.appConfigure.runStateBusy = true;
|
||||
app.runState = 'pending_stop'; // we assume we will end up there
|
||||
Client.stopApp(app.id, function () {
|
||||
$scope.appConfigure.runStateBusy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.cancel = function () {
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
location: '',
|
||||
portBindings: {},
|
||||
accessRestriction: '',
|
||||
oauthProxy: false,
|
||||
mediaLinks: []
|
||||
};
|
||||
|
||||
@@ -136,6 +137,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$scope.appInstall.location = '';
|
||||
$scope.appInstall.portBindings = {};
|
||||
$scope.appInstall.accessRestriction = '';
|
||||
$scope.appInstall.oauthProxy = false;
|
||||
$scope.appInstall.installFormVisible = false;
|
||||
$scope.appInstall.mediaLinks = [];
|
||||
$('#collapseInstallForm').collapse('hide');
|
||||
@@ -165,6 +167,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
|
||||
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
$scope.appInstall.accessRestriction = app.accessRestriction || '';
|
||||
$scope.appInstall.oauthProxy = app.oauthProxy || false;
|
||||
|
||||
// set default ports
|
||||
for (var env in $scope.appInstall.app.manifest.tcpPorts) {
|
||||
@@ -194,7 +197,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
}
|
||||
}
|
||||
|
||||
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, { location: $scope.appInstall.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appInstall.accessRestriction }, function (error) {
|
||||
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, { location: $scope.appInstall.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appInstall.accessRestriction, oauthProxy: $scope.appInstall.oauthProxy }, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
|
||||
$scope.appInstall.error.port = error.message;
|
||||
|
||||
Reference in New Issue
Block a user