Compare commits
386 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ae45381e2 | |||
| 1ae2e07883 | |||
| aa34850d4e | |||
| 9f524da642 | |||
| 8b707e23ca | |||
| a4ea693c3c | |||
| aca443a909 | |||
| 2ae5223da9 | |||
| b5b67f2e6a | |||
| fe723f5a53 | |||
| c55e1ff6b7 | |||
| 4bd88e1220 | |||
| f46af93528 | |||
| 8ead0e662a | |||
| 365ee01f96 | |||
| fca6de3997 | |||
| dceb265742 | |||
| 409096cbff | |||
| e5a40faf82 | |||
| 859c78c785 | |||
| 89bff16053 | |||
| a89476c538 | |||
| f51b61e407 | |||
| 177103bccd | |||
| f31d63aabd | |||
| fd20246e8b | |||
| 0c1ea39a02 | |||
| a409dd026d | |||
| 4731f8e5a7 | |||
| 7e05259b0e | |||
| 14ab85dc4f | |||
| 0651bfc4b8 | |||
| 21b94b2655 | |||
| 4e40c2341a | |||
| d9a83eacd2 | |||
| 7b40674c0d | |||
| 936c1989f1 | |||
| cfe336c37c | |||
| d8a1e4aab0 | |||
| be4d2afff3 | |||
| c2a4ef5f93 | |||
| 22634b4ceb | |||
| abc4975b3d | |||
| 36d81ff8d1 | |||
| fe94190c2f | |||
| f32027e15b | |||
| 4b6a92955b | |||
| 35a2da744c | |||
| 9d91340223 | |||
| e0a56f75c3 | |||
| 4cfd30f9e8 | |||
| 3fbcbf0e5d | |||
| 8b7833e8b1 | |||
| 66441f133d | |||
| 8a12d6019a | |||
| 39c626dc75 | |||
| a7480c3f29 | |||
| 8af682acf1 | |||
| 95eba1db81 | |||
| 0b8fde7d8d | |||
| 2f7517152a | |||
| 3e2ea0e087 | |||
| 723556d6a2 | |||
| 1f53d76cef | |||
| d15488431b | |||
| cf80fd7dc5 | |||
| 73d891b98e | |||
| 875ec1028d | |||
| fd985c2011 | |||
| 47981004c9 | |||
| e3f7c8f63d | |||
| 853db53f82 | |||
| 5992c0534a | |||
| 1874c93c5c | |||
| 3c4adb1aed | |||
| 66db918273 | |||
| 69845d5ddd | |||
| 42181d597b | |||
| b56e9ca745 | |||
| 5fc4788269 | |||
| d0f8293b73 | |||
| 44582bcd4b | |||
| 5c73aed953 | |||
| e1ec48530e | |||
| 54c4053728 | |||
| 79ffb0df5c | |||
| c510952c88 | |||
| 6109da531d | |||
| 56877332db | |||
| 6fc972d160 | |||
| 5346153d9b | |||
| aaf266d272 | |||
| 0750db9aae | |||
| 316976d295 | |||
| 593b5d945b | |||
| 88f0240757 | |||
| f5c2f8849d | |||
| 5c4a8f7803 | |||
| 5b8fdad5cb | |||
| fe819f95ec | |||
| be6728f8cb | |||
| 24d3a81bc8 | |||
| 268c7b5bcf | |||
| 64716a2de5 | |||
| d2c8457ab1 | |||
| 667cb84af7 | |||
| df8653cdd5 | |||
| 32f677ca0d | |||
| 6f5408f0d6 | |||
| 23c04fb10b | |||
| 0c5d6b1045 | |||
| 33f30decd1 | |||
| 9595b63939 | |||
| b9695b09cd | |||
| 5a0f7df377 | |||
| 2e54be3df8 | |||
| 6625610aca | |||
| 5c9abfe97a | |||
| e06f3d4180 | |||
| e3cc12da4f | |||
| 3d80821203 | |||
| d9bfcc7c8a | |||
| 8bd9a6c109 | |||
| d89db24bfc | |||
| 352b5ca736 | |||
| 6bd9173a9d | |||
| 0cef3e1090 | |||
| 6bd68961d1 | |||
| 7f8ad917d9 | |||
| 7cd89accaf | |||
| ffee084d2b | |||
| 2bb657a733 | |||
| bc48171626 | |||
| 50924b0cd3 | |||
| 3d86950cc9 | |||
| db9ddf9969 | |||
| 1b507370dc | |||
| 3c5e221c39 | |||
| 9c37f35d5a | |||
| 4044070d76 | |||
| 8f05917d97 | |||
| 3766d67daa | |||
| b1290c073e | |||
| 36daf86ea2 | |||
| 4fb07a6ab3 | |||
| 8f2119272b | |||
| ee5bd456e0 | |||
| 9c549ed4d8 | |||
| 61fc8b7968 | |||
| 6b30d65e05 | |||
| 10c876ac75 | |||
| 0966bd0bb1 | |||
| 294d1bfca4 | |||
| af1d1236ea | |||
| eaf9febdfd | |||
| 8748226ef3 | |||
| 73568777c0 | |||
| c64697dde7 | |||
| 0701e38a04 | |||
| 2a27d96e08 | |||
| ba42611701 | |||
| 54486138f0 | |||
| 13d3f506b0 | |||
| 32ca686e1f | |||
| a5ef9ff372 | |||
| 738bfa7601 | |||
| 40cdd270b1 | |||
| 53a2a8015e | |||
| 15aaa440a2 | |||
| d8a4014eff | |||
| d25d423ccd | |||
| 49b0fde18b | |||
| 8df7f17186 | |||
| adc395f888 | |||
| e770664365 | |||
| 05d4ad3b5d | |||
| cc6f726f71 | |||
| a4923f894c | |||
| 12200f2e0d | |||
| a853afc407 | |||
| de471b0012 | |||
| b6f1ad75b8 | |||
| e6840f352d | |||
| 6456874f97 | |||
| 66b4a4b02a | |||
| 7e36b3f8e5 | |||
| 12061cc707 | |||
| afcc62ecf6 | |||
| bec6850c98 | |||
| d253a06bab | |||
| 857c5c69b1 | |||
| 766fc49f39 | |||
| 941e09ca9f | |||
| 2466a97fb8 | |||
| 81f92f5182 | |||
| 91e1d442ff | |||
| a1d6ae2296 | |||
| b529fd3bea | |||
| bf319cf593 | |||
| 15eedd2a84 | |||
| d0cd3d1c32 | |||
| 747786d0c8 | |||
| b232255170 | |||
| bd2982ea69 | |||
| 1c948cc83c | |||
| ccde1e51ad | |||
| 03ec940352 | |||
| bd5b15e279 | |||
| b6897a4577 | |||
| f7225523ec | |||
| 9d9509525c | |||
| b1dbb3570b | |||
| c075160e5d | |||
| 612ceba98a | |||
| 7d5e0040bc | |||
| d6e19d2000 | |||
| a01d5db2a0 | |||
| 5de3baffd4 | |||
| 63c10e8f02 | |||
| a99e7c2783 | |||
| 88b1cc553f | |||
| 316e8dedd3 | |||
| f106a76cd5 | |||
| 95b2bea828 | |||
| 58d6166592 | |||
| d42f66bfed | |||
| 5bd8579e73 | |||
| 01cd0b6b87 | |||
| b4aec552fc | |||
| 93ab606d94 | |||
| 94e94f136d | |||
| 1b57128ef6 | |||
| 219a2b0798 | |||
| b37d5b0fda | |||
| 0e9aac14eb | |||
| cf81ab0306 | |||
| 00d8148e46 | |||
| 0b59281dbb | |||
| e0c845ca16 | |||
| d6bff57c7d | |||
| 5c4b4d764e | |||
| bf13b5b931 | |||
| afade0a5ac | |||
| 40da8736d4 | |||
| a55675b440 | |||
| 6ce71c7506 | |||
| 0dda91078d | |||
| 93632f5c76 | |||
| cb4cd10013 | |||
| 62bcf09ab4 | |||
| b466dc1970 | |||
| 0a10eb66cc | |||
| c6322c00aa | |||
| b549a4bddf | |||
| 3fa50f2a1a | |||
| ddded0ebfb | |||
| 71c0945607 | |||
| f0295c5dc5 | |||
| 4e1286a8cf | |||
| d69cead362 | |||
| 7699cffa26 | |||
| 1021fc566f | |||
| 1fb3b2c373 | |||
| 2428000262 | |||
| 3d5b4f3191 | |||
| eb6a217f4a | |||
| 06aaf98716 | |||
| 26fc1fd7a6 | |||
| a9aa3c4fd8 | |||
| 61d4509a8e | |||
| 8cff4f4ff1 | |||
| 5dc30e02c4 | |||
| 55f070e12c | |||
| 0afb8f51c3 | |||
| 42f2637078 | |||
| bbec7c6610 | |||
| 76fc257661 | |||
| 58ce50571a | |||
| 14205d2810 | |||
| d798fc4b3f | |||
| d29d07cb2d | |||
| 07a0b360f6 | |||
| 8b253a8a61 | |||
| fddbf96c9c | |||
| d1d01ae4b8 | |||
| 51706afc46 | |||
| d4ea23b1ac | |||
| 0460beccf0 | |||
| aa5ed17dfa | |||
| 32173b19c9 | |||
| 1a8fd7dd92 | |||
| f0047bc1aa | |||
| 917832e0ae | |||
| cf8948ac69 | |||
| b2df639155 | |||
| 70ace09ff5 | |||
| 35a69f595a | |||
| f4c4a931d2 | |||
| 7caced2fe8 | |||
| 846e5deb36 | |||
| eca328b247 | |||
| c0e9091e4b | |||
| 6b6e417435 | |||
| 954bb7039c | |||
| ae01f517c7 | |||
| 385bfe07e2 | |||
| 25aff6a53b | |||
| edcbf79b85 | |||
| 2591b8e10c | |||
| 9df9d1667f | |||
| 7798111af1 | |||
| 12351113a9 | |||
| d9256f99af | |||
| cf021066ed | |||
| 04eb2a982f | |||
| 22dcc787b5 | |||
| 5d4d0c0a86 | |||
| e81db9728a | |||
| db305af8c9 | |||
| 4b3aca7773 | |||
| 5b5abe99e7 | |||
| 8f670eb755 | |||
| 21a604814c | |||
| 7eeb835d96 | |||
| 57de915133 | |||
| a892de5c2d | |||
| 69cd01955b | |||
| f39809c941 | |||
| 09c4bfeb51 | |||
| 615789a9ad | |||
| bec5eaf3c9 | |||
| 4f13ef9cea | |||
| 873de48beb | |||
| 87e70b86d3 | |||
| 140aa85223 | |||
| 3ac3207497 | |||
| e36a0b9a30 | |||
| 0b1aac7687 | |||
| e008cde2ff | |||
| d1e46be8ad | |||
| dc18a18248 | |||
| b9a0ad73ab | |||
| e2c3fb309c | |||
| d5255b8cf4 | |||
| 42e70e870b | |||
| 8ffd7b0197 | |||
| 01ead194d8 | |||
| 80b9d4be50 | |||
| ef06836804 | |||
| 916870b546 | |||
| 2da7216be6 | |||
| 54215cff7a | |||
| 166257bbdc | |||
| d502e04cbd | |||
| 1fca680a67 | |||
| 4ea3238391 | |||
| fa12e7bd97 | |||
| 6118535c4a | |||
| 920f04aab3 | |||
| ed13f2d6ef | |||
| dff27fe7b3 | |||
| 5d589e7330 | |||
| 01ec16f472 | |||
| f510d4bc10 | |||
| 2db2eb13af | |||
| 82e1c07722 | |||
| 23ba078a17 | |||
| b5358e7565 | |||
| 697699bd5f | |||
| dd2a806ab8 | |||
| 84d96cebee | |||
| 10658606d7 | |||
| f72d89fa76 | |||
| f9f4a8e7ad | |||
| fd58e83da9 | |||
| bfcedfdb2a | |||
| d11e030150 | |||
| 6103640b53 | |||
| 259199897b | |||
| ee498b9e2b | |||
| 18a464b1d2 | |||
| d1c8e34540 | |||
| a151846f1c | |||
| 9f19b0bc9e | |||
| 289fe76adc | |||
| 1eb1c44926 |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -14,9 +14,9 @@ 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');
|
||||
server = require('./src/server.js'),
|
||||
simpleauth = require('./src/simpleauth.js');
|
||||
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
@@ -26,7 +26,6 @@ console.log();
|
||||
console.log(' Environment: ', config.CLOUDRON ? 'CLOUDRON' : 'TEST');
|
||||
console.log(' Version: ', config.version());
|
||||
console.log(' Admin Origin: ', config.adminOrigin());
|
||||
console.log(' Appstore token: ', config.token());
|
||||
console.log(' Appstore API server origin: ', config.apiServerOrigin());
|
||||
console.log(' Appstore Web server origin: ', config.webServerOrigin());
|
||||
console.log();
|
||||
|
||||
@@ -36,12 +36,7 @@ function main() {
|
||||
var processName = process.argv[2];
|
||||
console.log('Started crash notifier for', processName);
|
||||
|
||||
mailer.initialize(function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
sendCrashNotification(processName);
|
||||
});
|
||||
sendCrashNotification(processName);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps MODIFY manifestJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps MODIFY manifestJson VARCHAR(2048)', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY accessRestrictionJson TEXT'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY lastBackupConfigJson TEXT'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY oldConfigJson TEXT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY accessRestrictionJson VARCHAR(2048)'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY lastBackupConfigJson VARCHAR(2048)'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY oldConfigJson VARCHAR(2048)')
|
||||
], callback);
|
||||
};
|
||||
@@ -45,18 +45,18 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
runState VARCHAR(512),
|
||||
health VARCHAR(128),
|
||||
containerId VARCHAR(128),
|
||||
manifestJson VARCHAR(2048),
|
||||
manifestJson TEXT,
|
||||
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
|
||||
location VARCHAR(128) NOT NULL UNIQUE,
|
||||
dnsRecordId VARCHAR(512),
|
||||
accessRestrictionJson VARCHAR(2048),
|
||||
accessRestrictionJson TEXT,
|
||||
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
|
||||
lastBackupConfigJson TEXT, // 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
|
||||
oldConfigJson TEXT, // used to pass old config for apptask
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
"type": "git"
|
||||
},
|
||||
"engines": [
|
||||
"node >= 0.12.0"
|
||||
"node >=4.0.0 <=4.1.1"
|
||||
],
|
||||
"dependencies": {
|
||||
"async": "^1.2.1",
|
||||
"attempt": "^1.0.1",
|
||||
"aws-sdk": "^2.1.46",
|
||||
"body-parser": "^1.13.1",
|
||||
"bytes": "^2.1.0",
|
||||
"cloudron-manifestformat": "^2.0.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "0.0.13",
|
||||
@@ -50,18 +52,20 @@
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-oauth2-client-password": "^0.1.2",
|
||||
"password-generator": "^1.0.0",
|
||||
"password-generator": "^2.0.2",
|
||||
"proxy-middleware": "^0.13.0",
|
||||
"safetydance": "0.0.19",
|
||||
"safetydance": "^0.1.0",
|
||||
"semver": "^4.3.6",
|
||||
"serve-favicon": "^2.2.0",
|
||||
"split": "^1.0.0",
|
||||
"superagent": "~0.21.0",
|
||||
"supererror": "^0.7.0",
|
||||
"superagent": "^1.5.0",
|
||||
"supererror": "^0.7.1",
|
||||
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
|
||||
"underscore": "^1.7.0",
|
||||
"ursa": "^0.9.1",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^3.30.0"
|
||||
"validator": "^4.4.0",
|
||||
"x509": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"apidoc": "*",
|
||||
@@ -81,9 +85,9 @@
|
||||
"istanbul": "*",
|
||||
"js2xmlparser": "^1.0.0",
|
||||
"mocha": "*",
|
||||
"nock": "^2.6.0",
|
||||
"nock": "^3.4.0",
|
||||
"node-sass": "^3.0.0-alpha.0",
|
||||
"redis": "^0.12.1",
|
||||
"redis": "^2.4.2",
|
||||
"request": "^2.65.0",
|
||||
"sinon": "^1.12.2",
|
||||
"yargs": "^3.15.0"
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
# If you change the infra version, be sure to put a warning
|
||||
# in the change log
|
||||
|
||||
INFRA_VERSION=18
|
||||
INFRA_VERSION=21
|
||||
|
||||
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
|
||||
# These constants are used in the installer script as well
|
||||
BASE_IMAGE=cloudron/base:0.7.0
|
||||
MYSQL_IMAGE=cloudron/mysql:0.7.0
|
||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.7.0
|
||||
MONGODB_IMAGE=cloudron/mongodb:0.7.0
|
||||
REDIS_IMAGE=cloudron/redis:0.7.0 # if you change this, fix src/addons.js as well
|
||||
MAIL_IMAGE=cloudron/mail:0.7.0
|
||||
GRAPHITE_IMAGE=cloudron/graphite:0.7.0
|
||||
BASE_IMAGE=cloudron/base:0.8.0
|
||||
MYSQL_IMAGE=cloudron/mysql:0.8.0
|
||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.8.0
|
||||
MONGODB_IMAGE=cloudron/mongodb:0.8.0
|
||||
REDIS_IMAGE=cloudron/redis:0.8.0 # if you change this, fix src/addons.js as well
|
||||
MAIL_IMAGE=cloudron/mail:0.9.0
|
||||
GRAPHITE_IMAGE=cloudron/graphite:0.8.0
|
||||
|
||||
MYSQL_REPO=cloudron/mysql
|
||||
POSTGRESQL_REPO=cloudron/postgresql
|
||||
|
||||
@@ -11,13 +11,14 @@ arg_is_custom_domain="false"
|
||||
arg_restore_key=""
|
||||
arg_restore_url=""
|
||||
arg_retire="false"
|
||||
arg_tls_config=""
|
||||
arg_tls_cert=""
|
||||
arg_tls_key=""
|
||||
arg_token=""
|
||||
arg_version=""
|
||||
arg_web_server_origin=""
|
||||
arg_backup_key=""
|
||||
arg_aws=""
|
||||
arg_backup_config=""
|
||||
arg_dns_config=""
|
||||
|
||||
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
@@ -37,17 +38,20 @@ EOF
|
||||
arg_tls_cert=$(echo "$2" | $json tlsCert)
|
||||
arg_tls_key=$(echo "$2" | $json tlsKey)
|
||||
|
||||
arg_restore_url=$(echo "$2" | $json restoreUrl)
|
||||
arg_tls_config=$(echo "$2" | $json tlsConfig)
|
||||
[[ "${arg_tls_config}" == "null" ]] && arg_tls_config=""
|
||||
|
||||
arg_restore_url=$(echo "$2" | $json restore.url)
|
||||
[[ "${arg_restore_url}" == "null" ]] && arg_restore_url=""
|
||||
|
||||
arg_restore_key=$(echo "$2" | $json restoreKey)
|
||||
arg_restore_key=$(echo "$2" | $json restore.key)
|
||||
[[ "${arg_restore_key}" == "null" ]] && arg_restore_key=""
|
||||
|
||||
arg_backup_key=$(echo "$2" | $json backupKey)
|
||||
[[ "${arg_backup_key}" == "null" ]] && arg_backup_key=""
|
||||
arg_backup_config=$(echo "$2" | $json backupConfig)
|
||||
[[ "${arg_backup_config}" == "null" ]] && arg_backup_config=""
|
||||
|
||||
arg_aws=$(echo "$2" | $json aws)
|
||||
[[ "${arg_aws}" == "null" ]] && arg_aws=""
|
||||
arg_dns_config=$(echo "$2" | $json dnsConfig)
|
||||
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
|
||||
|
||||
shift 2
|
||||
;;
|
||||
@@ -66,5 +70,6 @@ echo "restore url: ${arg_restore_url}"
|
||||
echo "tls cert: ${arg_tls_cert}"
|
||||
echo "tls key: ${arg_tls_key}"
|
||||
echo "token: ${arg_token}"
|
||||
echo "tlsConfig: ${arg_tls_config}"
|
||||
echo "version: ${arg_version}"
|
||||
echo "web server: ${arg_web_server_origin}"
|
||||
|
||||
@@ -14,4 +14,6 @@ User=yellowtent
|
||||
Group=yellowtent
|
||||
MemoryLimit=200M
|
||||
TimeoutStopSec=5s
|
||||
StartLimitInterval=1
|
||||
StartLimitBurst=60
|
||||
|
||||
|
||||
@@ -29,10 +29,10 @@ infra_version="none"
|
||||
if [[ "${arg_retire}" == "true" || "${infra_version}" != "${INFRA_VERSION}" ]]; then
|
||||
rm -f ${DATA_DIR}/nginx/applications/*
|
||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
else
|
||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
fi
|
||||
|
||||
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
|
||||
|
||||
@@ -38,7 +38,9 @@ set_progress "10" "Ensuring directories"
|
||||
# keep these in sync with paths.js
|
||||
[[ "${is_update}" == "false" ]] && btrfs subvolume create "${DATA_DIR}/box"
|
||||
mkdir -p "${DATA_DIR}/box/appicons"
|
||||
mkdir -p "${DATA_DIR}/box/certs"
|
||||
mkdir -p "${DATA_DIR}/box/mail"
|
||||
mkdir -p "${DATA_DIR}/box/acme" # acme keys
|
||||
mkdir -p "${DATA_DIR}/graphite"
|
||||
|
||||
mkdir -p "${DATA_DIR}/mysql"
|
||||
@@ -47,6 +49,7 @@ mkdir -p "${DATA_DIR}/mongodb"
|
||||
mkdir -p "${DATA_DIR}/snapshots"
|
||||
mkdir -p "${DATA_DIR}/addons"
|
||||
mkdir -p "${DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${DATA_DIR}/acme" # acme challenges
|
||||
|
||||
# bookkeep the version as part of data
|
||||
echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_versions_url}\" }" > "${DATA_DIR}/box/version"
|
||||
@@ -96,24 +99,32 @@ ln -sfF "df-disk_by-uuid_${vda1_id}" "${DATA_DIR}/graphite/whisper/collectd/loca
|
||||
service collectd restart
|
||||
|
||||
set_progress "30" "Setup nginx"
|
||||
# setup naked domain to use admin by default. app restoration will overwrite this config
|
||||
mkdir -p "${DATA_DIR}/nginx/applications"
|
||||
cp "${script_dir}/start/nginx/nginx.conf" "${DATA_DIR}/nginx/nginx.conf"
|
||||
cp "${script_dir}/start/nginx/mime.types" "${DATA_DIR}/nginx/mime.types"
|
||||
|
||||
# generate the main nginx config file
|
||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/nginx.ejs" \
|
||||
-O "{ \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/nginx.conf"
|
||||
|
||||
# generate these for update code paths as well to overwrite splash
|
||||
admin_cert_file="${DATA_DIR}/nginx/cert/host.cert"
|
||||
admin_key_file="${DATA_DIR}/nginx/cert/host.key"
|
||||
if [[ -f "${DATA_DIR}/box/certs/${admin_fqdn}.cert" && -f "${DATA_DIR}/box/certs/${admin_fqdn}.key" ]]; then
|
||||
admin_cert_file="${DATA_DIR}/box/certs/${admin_fqdn}.cert"
|
||||
admin_key_file="${DATA_DIR}/box/certs/${admin_fqdn}.key"
|
||||
fi
|
||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"${admin_cert_file}\", \"keyFilePath\": \"${admin_key_file}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
|
||||
mkdir -p "${DATA_DIR}/nginx/cert"
|
||||
echo "${arg_tls_cert}" > ${DATA_DIR}/nginx/cert/host.cert
|
||||
echo "${arg_tls_key}" > ${DATA_DIR}/nginx/cert/host.key
|
||||
if [[ -f "${DATA_DIR}/box/certs/host.cert" && -f "${DATA_DIR}/box/certs/host.key" ]]; then
|
||||
cp "${DATA_DIR}/box/certs/host.cert" "${DATA_DIR}/nginx/cert/host.cert"
|
||||
cp "${DATA_DIR}/box/certs/host.key" "${DATA_DIR}/nginx/cert/host.key"
|
||||
else
|
||||
echo "${arg_tls_cert}" > "${DATA_DIR}/nginx/cert/host.cert"
|
||||
echo "${arg_tls_key}" > "${DATA_DIR}/nginx/cert/host.key"
|
||||
fi
|
||||
|
||||
set_progress "33" "Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons"
|
||||
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
|
||||
chown "${USER}:${USER}" "${DATA_DIR}"
|
||||
|
||||
set_progress "40" "Setting up infra"
|
||||
${script_dir}/start/setup_infra.sh "${arg_fqdn}"
|
||||
@@ -122,7 +133,6 @@ set_progress "65" "Creating cloudron.conf"
|
||||
sudo -u yellowtent -H bash <<EOF
|
||||
set -eu
|
||||
echo "Creating cloudron.conf"
|
||||
# note that arg_aws is a javascript object and intentionally unquoted below
|
||||
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
{
|
||||
"version": "${arg_version}",
|
||||
@@ -139,9 +149,7 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
"password": "${mysql_root_password}",
|
||||
"port": 3306,
|
||||
"name": "box"
|
||||
},
|
||||
"backupKey": "${arg_backup_key}",
|
||||
"aws": ${arg_aws}
|
||||
}
|
||||
}
|
||||
CONF_END
|
||||
|
||||
@@ -153,6 +161,30 @@ cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
|
||||
CONF_END
|
||||
EOF
|
||||
|
||||
# Add Backup Configuration
|
||||
if [[ ! -z "${arg_backup_config}" ]]; then
|
||||
echo "Add Backup Config"
|
||||
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
|
||||
fi
|
||||
|
||||
# Add DNS Configuration
|
||||
if [[ ! -z "${arg_dns_config}" ]]; then
|
||||
echo "Add DNS Config"
|
||||
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
|
||||
fi
|
||||
|
||||
# Add TLS Configuration
|
||||
if [[ ! -z "${arg_tls_config}" ]]; then
|
||||
echo "Add TLS Config"
|
||||
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
|
||||
fi
|
||||
|
||||
# Add webadmin oauth client
|
||||
# The domain might have changed, therefor we have to update the record
|
||||
# !!! This needs to be in sync with the webadmin, specifically login_callback.js
|
||||
|
||||
@@ -10,8 +10,8 @@ server {
|
||||
|
||||
ssl on;
|
||||
# paths are relative to prefix and not to this file
|
||||
ssl_certificate cert/host.cert;
|
||||
ssl_certificate_key cert/host.key;
|
||||
ssl_certificate <%= certFilePath %>;
|
||||
ssl_certificate_key <%= keyFilePath %>;
|
||||
ssl_session_timeout 5m;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
|
||||
@@ -37,7 +37,8 @@ server {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
error_page 500 502 503 504 @appstatus;
|
||||
# only serve up the status page if we get proxy gateway errors
|
||||
error_page 502 503 504 @appstatus;
|
||||
location @appstatus {
|
||||
return 307 <%= adminOrigin %>/appstatus.html?referrer=https://$host$request_uri;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,12 @@ http {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# acme challenges
|
||||
location /.well-known/acme-challenge/ {
|
||||
default_type text/plain;
|
||||
alias /home/yellowtent/data/acme/;
|
||||
}
|
||||
|
||||
location / {
|
||||
# redirect everything to HTTPS
|
||||
return 301 https://$host$request_uri;
|
||||
@@ -55,7 +61,7 @@ http {
|
||||
error_page 404 = @fallback;
|
||||
location @fallback {
|
||||
internal;
|
||||
root <%= sourceDir %>/webadmin/dist;
|
||||
root /home/yellowtent/box/webadmin/dist;
|
||||
rewrite ^/$ /nakeddomain.html break;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ exports = module.exports = {
|
||||
getEnvironment: getEnvironment,
|
||||
getLinksSync: getLinksSync,
|
||||
getBindsSync: getBindsSync,
|
||||
getContainerNamesSync: getContainerNamesSync,
|
||||
|
||||
// exported for testing
|
||||
_setupOauth: setupOauth,
|
||||
@@ -239,6 +240,27 @@ function getBindsSync(app, addons) {
|
||||
return binds;
|
||||
}
|
||||
|
||||
function getContainerNamesSync(app, addons) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addons || typeof addons === 'object');
|
||||
|
||||
var names = [ ];
|
||||
|
||||
if (!addons) return names;
|
||||
|
||||
for (var addon in addons) {
|
||||
switch (addon) {
|
||||
case 'scheduler':
|
||||
// names here depend on how scheduler.js creates containers
|
||||
names = names.concat(Object.keys(addons.scheduler).map(function (taskName) { return app.id + '-' + taskName; }));
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
function setupOauth(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
@@ -303,10 +325,10 @@ function setupSimpleAuth(app, options, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
'SIMPLE_AUTH_SERVER=172.17.42.1',
|
||||
'SIMPLE_AUTH_SERVER=172.17.0.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_URL=http://172.17.0.1:' + config.get('simpleAuthPort'), // obsolete, remove
|
||||
'SIMPLE_AUTH_ORIGIN=http://172.17.0.1:' + config.get('simpleAuthPort'),
|
||||
'SIMPLE_AUTH_CLIENT_ID=' + id
|
||||
];
|
||||
|
||||
@@ -337,9 +359,9 @@ function setupLdap(app, options, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var env = [
|
||||
'LDAP_SERVER=172.17.42.1',
|
||||
'LDAP_SERVER=172.17.0.1',
|
||||
'LDAP_PORT=' + config.get('ldapPort'),
|
||||
'LDAP_URL=ldap://172.17.42.1:' + config.get('ldapPort'),
|
||||
'LDAP_URL=ldap://172.17.0.1:' + config.get('ldapPort'),
|
||||
'LDAP_USERS_BASE_DN=ou=users,dc=cloudron',
|
||||
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron',
|
||||
'LDAP_BIND_DN=cn='+ app.id + ',ou=apps,dc=cloudron',
|
||||
@@ -369,7 +391,7 @@ function setupSendMail(app, options, callback) {
|
||||
var env = [
|
||||
'MAIL_SMTP_SERVER=mail',
|
||||
'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_SMTP_USERNAME=' + (app.location || app.id) + '-app', // use app.id for bare domains
|
||||
'MAIL_DOMAIN=' + config.fqdn()
|
||||
];
|
||||
|
||||
@@ -754,9 +776,9 @@ function setupRedis(app, options, callback) {
|
||||
|
||||
var createOptions = {
|
||||
name: 'redis-' + app.id,
|
||||
Hostname: config.appFqdn(app.location),
|
||||
Hostname: 'redis-' + app.location,
|
||||
Tty: true,
|
||||
Image: 'cloudron/redis:0.7.0', // if you change this, fix setup/INFRA_VERSION as well
|
||||
Image: 'cloudron/redis:0.8.0', // if you change this, fix setup/INFRA_VERSION as well
|
||||
Cmd: null,
|
||||
Volumes: {
|
||||
'/tmp': {},
|
||||
|
||||
@@ -92,12 +92,13 @@ function checkAppHealth(app, callback) {
|
||||
.redirects(0)
|
||||
.timeout(HEALTHCHECK_INTERVAL)
|
||||
.end(function (error, res) {
|
||||
|
||||
if (error || res.status >= 400) { // 2xx and 3xx are ok
|
||||
if (error && !error.response) {
|
||||
debugApp(app, 'not alive (network error): %s', error.message);
|
||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
|
||||
debugApp(app, 'not alive : %s', error || res.status);
|
||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||
} else {
|
||||
debugApp(app, 'alive');
|
||||
setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
||||
}
|
||||
});
|
||||
@@ -110,6 +111,13 @@ function processApps(callback) {
|
||||
|
||||
async.each(apps, checkAppHealth, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
var alive = apps
|
||||
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
|
||||
.map(function (a) { return a.location; }).join(', ');
|
||||
|
||||
debug('apps alive: [%s]', alive);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
@@ -149,7 +157,7 @@ function processDockerEvents() {
|
||||
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
|
||||
if (error || app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash
|
||||
});
|
||||
});
|
||||
|
||||
@@ -159,9 +167,8 @@ function processDockerEvents() {
|
||||
});
|
||||
|
||||
stream.on('end', function () {
|
||||
console.error('Docke event stream ended');
|
||||
console.error('Docker event stream ended');
|
||||
gDockerEventStream = null; // TODO: reconnect?
|
||||
stream.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ exports = module.exports = {
|
||||
backup: backup,
|
||||
backupApp: backupApp,
|
||||
|
||||
getLogStream: getLogStream,
|
||||
getLogs: getLogs,
|
||||
|
||||
start: start,
|
||||
@@ -49,18 +48,21 @@ var addons = require('./addons.js'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
certificates = require('./certificates.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:apps'),
|
||||
docker = require('./docker.js').connection,
|
||||
docker = require('./docker.js'),
|
||||
fs = require('fs'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
once = require('once'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
superagent = require('superagent'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
@@ -71,6 +73,8 @@ var BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
|
||||
RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'),
|
||||
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
@@ -119,6 +123,7 @@ AppsError.PORT_CONFLICT = 'Port Conflict';
|
||||
AppsError.BILLING_REQUIRED = 'Billing Required';
|
||||
AppsError.ACCESS_DENIED = 'Access denied';
|
||||
AppsError.USER_REQUIRED = 'User required';
|
||||
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
|
||||
|
||||
// Hostname validation comes from RFC 1123 (section 2.1)
|
||||
// Domain name validation comes from RFC 2181 (Name syntax)
|
||||
@@ -171,7 +176,7 @@ function validatePortBindings(portBindings, tcpPorts) {
|
||||
if (!Number.isInteger(portBindings[env])) return new Error(portBindings[env] + ' is not an integer');
|
||||
if (portBindings[env] <= 0 || portBindings[env] > 65535) return new Error(portBindings[env] + ' is out of range');
|
||||
|
||||
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, + portBindings[env]);
|
||||
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
|
||||
}
|
||||
|
||||
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
|
||||
@@ -286,15 +291,16 @@ function purchase(appStoreId, callback) {
|
||||
var url = config.apiServerOrigin() + '/api/v1/apps/' + appStoreId + '/purchase';
|
||||
|
||||
superagent.post(url).query({ token: config.token() }).end(function (error, res) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
if (res.status === 402) return callback(new AppsError(AppsError.BILLING_REQUIRED));
|
||||
if (res.status !== 201 && res.status !== 200) return callback(new Error(util.format('App purchase failed. %s %j', res.status, res.body)));
|
||||
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error));
|
||||
if (res.statusCode === 402) return callback(new AppsError(AppsError.BILLING_REQUIRED));
|
||||
if (res.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (res.statusCode !== 201 && res.statusCode !== 200) return callback(new Error(util.format('App purchase failed. %s %j', res.status, res.body)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, icon, callback) {
|
||||
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, icon, cert, key, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
@@ -303,6 +309,8 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
assert.strictEqual(typeof accessRestriction, 'object');
|
||||
assert.strictEqual(typeof oauthProxy, 'boolean');
|
||||
assert(!icon || typeof icon === 'string');
|
||||
assert(cert === null || typeof cert === 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = manifestFormat.parse(manifest);
|
||||
@@ -332,6 +340,9 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
}
|
||||
}
|
||||
|
||||
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
debug('Will install app with id : ' + appId);
|
||||
|
||||
purchase(appStoreId, function (error) {
|
||||
@@ -341,6 +352,12 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
// save cert to data/box/certs
|
||||
if (cert && key) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
}
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
callback(null);
|
||||
@@ -348,12 +365,14 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
});
|
||||
}
|
||||
|
||||
function configure(appId, location, portBindings, accessRestriction, oauthProxy, callback) {
|
||||
function configure(appId, location, portBindings, accessRestriction, oauthProxy, cert, key, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof accessRestriction, 'object');
|
||||
assert.strictEqual(typeof oauthProxy, 'boolean');
|
||||
assert(cert === null || typeof cert === 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateHostname(location, config.fqdn());
|
||||
@@ -362,6 +381,9 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
|
||||
error = validateAccessRestriction(accessRestriction);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
@@ -369,6 +391,12 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
|
||||
error = validatePortBindings(portBindings, app.manifest.tcpPorts);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
// save cert to data/box/certs
|
||||
if (cert && key) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
}
|
||||
|
||||
var values = {
|
||||
location: location.toLowerCase(),
|
||||
accessRestriction: accessRestriction,
|
||||
@@ -451,58 +479,48 @@ function update(appId, force, manifest, portBindings, icon, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getLogStream(appId, fromLine, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof fromLine, 'number'); // behaves like tail -n
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
function appLogFilter(app) {
|
||||
var names = [ app.id ].concat(addons.getContainerNamesSync(app, app.manifest.addons));
|
||||
|
||||
debug('Getting logs for %s', appId);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState)));
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
var tail = fromLine < 0 ? -fromLine : 'all';
|
||||
|
||||
// note: cannot access docker file directly because it needs root access
|
||||
container.logs({ stdout: true, stderr: true, follow: true, timestamps: true, tail: tail }, function (error, logStream) {
|
||||
if (error && error.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var lineCount = 0;
|
||||
var skipLinesStream = split(function mapper(line) {
|
||||
if (++lineCount < fromLine) return undefined;
|
||||
var timestamp = line.substr(0, line.indexOf(' ')); // sometimes this has square brackets around it
|
||||
return JSON.stringify({ lineNumber: lineCount, timestamp: timestamp.replace(/[[\]]/g,''), log: line.substr(timestamp.length + 1) });
|
||||
});
|
||||
skipLinesStream.close = logStream.req.abort;
|
||||
logStream.pipe(skipLinesStream);
|
||||
return callback(null, skipLinesStream);
|
||||
});
|
||||
});
|
||||
return names.map(function (name) { return 'CONTAINER_NAME=' + name; });
|
||||
}
|
||||
|
||||
function getLogs(appId, callback) {
|
||||
function getLogs(appId, lines, follow, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof follow, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Getting logs for %s', appId);
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState)));
|
||||
var args = [ '--output=json', '--no-pager', '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
args = args.concat(appLogFilter(app));
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
// note: cannot access docker file directly because it needs root access
|
||||
container.logs({ stdout: true, stderr: true, follow: false, timestamps: true, tail: 'all' }, function (error, logStream) {
|
||||
if (error && error.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
var cp = spawn('/bin/journalctl', args);
|
||||
|
||||
return callback(null, logStream);
|
||||
var transformStream = split(function mapper(line) {
|
||||
var obj = safe.JSON.parse(line);
|
||||
if (!obj) return undefined;
|
||||
|
||||
var source = obj.CONTAINER_NAME.slice(app.id.length + 1);
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
|
||||
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
|
||||
message: obj.MESSAGE,
|
||||
source: source || 'main'
|
||||
}) + '\n';
|
||||
});
|
||||
|
||||
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
|
||||
|
||||
cp.stdout.pipe(transformStream);
|
||||
|
||||
return callback(null, transformStream);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -627,31 +645,42 @@ function exec(appId, options, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
|
||||
var execOptions = {
|
||||
var createOptions = {
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
Cmd: cmd
|
||||
OpenStdin: true,
|
||||
StdinOnce: false,
|
||||
Tty: true
|
||||
};
|
||||
|
||||
container.exec(execOptions, function (error, exec) {
|
||||
docker.createSubcontainer(app, app.id + '-exec-' + Date.now(), cmd, createOptions, function (error, container) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
var startOptions = {
|
||||
Detach: false,
|
||||
Tty: true,
|
||||
stdin: true // this is a dockerode option that enabled openStdin in the modem
|
||||
};
|
||||
exec.start(startOptions, function(error, stream) {
|
||||
|
||||
container.attach({ stream: true, stdin: true, stdout: true, stderr: true }, function (error, stream) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (options.rows && options.columns) {
|
||||
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
|
||||
}
|
||||
docker.startContainer(container.id, function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, stream);
|
||||
if (options.rows && options.columns) {
|
||||
container.resize({ h: options.rows, w: options.columns }, NOOP_CALLBACK);
|
||||
}
|
||||
|
||||
var deleteContainer = once(docker.deleteContainer.bind(null, container.id, NOOP_CALLBACK));
|
||||
|
||||
container.wait(function (error) {
|
||||
if (error) debug('Error waiting on container', error);
|
||||
|
||||
debug('exec: container finished', container.id);
|
||||
|
||||
deleteContainer();
|
||||
});
|
||||
|
||||
stream.close = deleteContainer;
|
||||
|
||||
callback(null, stream);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ exports = module.exports = {
|
||||
startTask: startTask,
|
||||
|
||||
// exported for testing
|
||||
_getFreePort: getFreePort,
|
||||
_reserveHttpPort: reserveHttpPort,
|
||||
_configureNginx: configureNginx,
|
||||
_unconfigureNginx: unconfigureNginx,
|
||||
_createVolume: createVolume,
|
||||
@@ -19,7 +19,6 @@ exports = module.exports = {
|
||||
_verifyManifest: verifyManifest,
|
||||
_registerSubdomain: registerSubdomain,
|
||||
_unregisterSubdomain: unregisterSubdomain,
|
||||
_reloadNginx: reloadNginx,
|
||||
_waitForDnsPropagation: waitForDnsPropagation
|
||||
};
|
||||
|
||||
@@ -36,6 +35,7 @@ var addons = require('./addons.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
certificates = require('./certificates.js'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
config = require('./config.js'),
|
||||
database = require('./database.js'),
|
||||
@@ -47,12 +47,12 @@ var addons = require('./addons.js'),
|
||||
hat = require('hat'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
net = require('net'),
|
||||
nginx = require('./nginx.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
shell = require('./shell.js'),
|
||||
SubdomainError = require('./subdomainerror.js'),
|
||||
SubdomainError = require('./subdomains.js').SubdomainError,
|
||||
subdomains = require('./subdomains.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
@@ -60,9 +60,7 @@ var addons = require('./addons.js'),
|
||||
uuid = require('node-uuid'),
|
||||
_ = require('underscore');
|
||||
|
||||
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
|
||||
COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
|
||||
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_COLLECTD_CMD = path.join(__dirname, 'scripts/reloadcollectd.sh'),
|
||||
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
|
||||
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
|
||||
@@ -78,61 +76,34 @@ 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) {
|
||||
function reserveHttpPort(app, callback) {
|
||||
var server = net.createServer();
|
||||
server.listen(0, function () {
|
||||
var port = server.address().port;
|
||||
server.close(function () {
|
||||
return callback(null, port);
|
||||
updateApp(app, { httpPort: port }, function (error) {
|
||||
if (error) {
|
||||
server.close();
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
server.close(callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reloadNginx(callback) {
|
||||
shell.sudo('reloadNginx', [ RELOAD_NGINX_CMD ], callback);
|
||||
}
|
||||
function configureNginx(app, callback) {
|
||||
var vhost = config.appFqdn(app.location);
|
||||
|
||||
function configureNginx(app, callback) {
|
||||
getFreePort(function (error, freePort) {
|
||||
certificates.ensureCertificate(vhost, function (error, certFilePath, keyFilePath) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var sourceDir = path.resolve(__dirname, '..');
|
||||
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');
|
||||
debugApp(app, 'writing config to %s', nginxConfigFilename);
|
||||
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
|
||||
debugApp(app, 'Error creating nginx config : %s', safe.error.message);
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
async.series([
|
||||
exports._reloadNginx,
|
||||
updateApp.bind(null, app, { httpPort: freePort })
|
||||
], callback);
|
||||
nginx.configureApp(app, certFilePath, keyFilePath, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function unconfigureNginx(app, callback) {
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
|
||||
debugApp(app, 'Error removing nginx configuration : %s', safe.error.message);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
exports._reloadNginx(callback);
|
||||
// TODO: maybe revoke the cert
|
||||
nginx.unconfigureApp(app, callback);
|
||||
}
|
||||
|
||||
function createContainer(app, callback) {
|
||||
@@ -230,8 +201,8 @@ function downloadIcon(app, callback) {
|
||||
.get(iconUrl)
|
||||
.buffer(true)
|
||||
.end(function (error, res) {
|
||||
if (error) return callback(new Error('Error downloading icon:' + error.message));
|
||||
if (res.status !== 200) return callback(null); // ignore error. this can also happen for apps installed with cloudron-cli
|
||||
if (error && !error.response) return callback(new Error('Network error downloading icon:' + error.message));
|
||||
if (res.statusCode !== 200) return callback(null); // ignore error. this can also happen for apps installed with cloudron-cli
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, app.id + '.png'), res.body)) return callback(new Error('Error saving icon:' + safe.error.message));
|
||||
|
||||
@@ -242,12 +213,10 @@ function downloadIcon(app, callback) {
|
||||
function registerSubdomain(app, callback) {
|
||||
// even though the bare domain is already registered in the appstore, we still
|
||||
// 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: 200, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Registering subdomain location [%s]', app.location);
|
||||
|
||||
subdomains.add(record, function (error, changeId) {
|
||||
subdomains.add(app.location, 'A', [ sysinfo.getIp() ], function (error, changeId) {
|
||||
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
|
||||
retryCallback(null, error || changeId);
|
||||
@@ -266,18 +235,16 @@ function unregisterSubdomain(app, location, callback) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
var record = { subdomain: location, type: 'A', value: sysinfo.getIp() };
|
||||
|
||||
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Unregistering subdomain: %s', location);
|
||||
|
||||
subdomains.remove(record, function (error) {
|
||||
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR))return retryCallback(error); // try again
|
||||
subdomains.remove(location, 'A', [ sysinfo.getIp() ], function (error) {
|
||||
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
|
||||
retryCallback(error);
|
||||
retryCallback(null, error);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) debugApp(app, 'Error unregistering subdomain: %s', error);
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
|
||||
updateApp(app, { dnsRecordId: null }, callback);
|
||||
});
|
||||
@@ -343,6 +310,7 @@ function install(app, callback) {
|
||||
|
||||
// teardown for re-installs
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
@@ -351,10 +319,8 @@ function install(app, callback) {
|
||||
unregisterSubdomain.bind(null, app, app.location),
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
// removeIcon.bind(null, app), // do not remove icon for non-appstore installs
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '15, Configure nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
@@ -385,6 +351,9 @@ function install(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'installed');
|
||||
@@ -429,6 +398,7 @@ function restore(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
@@ -442,10 +412,8 @@ function restore(app, callback) {
|
||||
},
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
removeIcon.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '30, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
@@ -476,6 +444,9 @@ function restore(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'restored');
|
||||
@@ -495,6 +466,7 @@ function restore(app, callback) {
|
||||
function configure(app, callback) {
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
@@ -504,10 +476,8 @@ function configure(app, callback) {
|
||||
unregisterSubdomain(app, app.oldConfig.location, next);
|
||||
},
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '25, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '30, Create OAuth proxy credentials' }),
|
||||
allocateOAuthProxyCredentials.bind(null, app),
|
||||
@@ -530,6 +500,9 @@ function configure(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'configured');
|
||||
@@ -660,7 +633,7 @@ function stopApp(app, callback) {
|
||||
docker.stopContainers(app.id, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
updateApp(app, { runState: appdb.RSTATE_STOPPED }, callback);
|
||||
updateApp(app, { runState: appdb.RSTATE_STOPPED, health: null }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -714,7 +687,7 @@ if (require.main === module) {
|
||||
if (error) throw error;
|
||||
|
||||
startTask(process.argv[2], function (error) {
|
||||
if (error) console.error(error);
|
||||
if (error) debug('Apptask completed with error', error);
|
||||
|
||||
debug('Apptask completed for %s', process.argv[2]);
|
||||
// https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below
|
||||
@@ -723,4 +696,3 @@ if (require.main === module) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getSignedUploadUrl: getSignedUploadUrl,
|
||||
getSignedDownloadUrl: getSignedDownloadUrl,
|
||||
|
||||
addSubdomain: addSubdomain,
|
||||
delSubdomain: delSubdomain,
|
||||
getChangeStatus: getChangeStatus,
|
||||
|
||||
copyObject: copyObject
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
AWS = require('aws-sdk'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:aws'),
|
||||
SubdomainError = require('./subdomainerror.js'),
|
||||
superagent = require('superagent');
|
||||
|
||||
function getAWSCredentials(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// CaaS
|
||||
if (config.token()) {
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
|
||||
superagent.post(url).query({ token: config.token() }).end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.statusCode !== 201) return callback(new Error(result.text));
|
||||
if (!result.body || !result.body.credentials) return callback(new Error('Unexpected response'));
|
||||
|
||||
var credentials = {
|
||||
accessKeyId: result.body.credentials.AccessKeyId,
|
||||
secretAccessKey: result.body.credentials.SecretAccessKey,
|
||||
sessionToken: result.body.credentials.SessionToken,
|
||||
region: 'us-east-1'
|
||||
};
|
||||
|
||||
if (config.aws().endpoint) credentials.endpoint = new AWS.Endpoint(config.aws().endpoint);
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
} else {
|
||||
if (!config.aws().accessKeyId || !config.aws().secretAccessKey) return callback(new SubdomainError(SubdomainError.MISSING_CREDENTIALS));
|
||||
|
||||
var credentials = {
|
||||
accessKeyId: config.aws().accessKeyId,
|
||||
secretAccessKey: config.aws().secretAccessKey,
|
||||
region: 'us-east-1'
|
||||
};
|
||||
|
||||
if (config.aws().endpoint) credentials.endpoint = new AWS.Endpoint(config.aws().endpoint);
|
||||
|
||||
callback(null, credentials);
|
||||
}
|
||||
}
|
||||
|
||||
function getSignedUploadUrl(filename, callback) {
|
||||
assert.strictEqual(typeof filename, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getSignedUploadUrl: %s', filename);
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
|
||||
var params = {
|
||||
Bucket: config.aws().backupBucket,
|
||||
Key: config.aws().backupPrefix + '/' + filename,
|
||||
Expires: 60 * 30 /* 30 minutes */
|
||||
};
|
||||
|
||||
var url = s3.getSignedUrl('putObject', params);
|
||||
|
||||
callback(null, { url : url, sessionToken: credentials.sessionToken });
|
||||
});
|
||||
}
|
||||
|
||||
function getSignedDownloadUrl(filename, callback) {
|
||||
assert.strictEqual(typeof filename, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getSignedDownloadUrl: %s', filename);
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
|
||||
var params = {
|
||||
Bucket: config.aws().backupBucket,
|
||||
Key: config.aws().backupPrefix + '/' + filename,
|
||||
Expires: 60 * 30 /* 30 minutes */
|
||||
};
|
||||
|
||||
var url = s3.getSignedUrl('getObject', params);
|
||||
|
||||
callback(null, { url: url, sessionToken: credentials.sessionToken });
|
||||
});
|
||||
}
|
||||
|
||||
function getZoneByName(zoneName, callback) {
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getZoneByName: %s', zoneName);
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(credentials);
|
||||
route53.listHostedZones({}, function (error, result) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
|
||||
var zone = result.HostedZones.filter(function (zone) {
|
||||
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
|
||||
})[0];
|
||||
|
||||
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
|
||||
|
||||
debug('getZoneByName: found zone', zone);
|
||||
|
||||
callback(null, zone);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('addSubdomain: ' + subdomain + ' for domain ' + zoneName + ' with value ' + value);
|
||||
|
||||
getZoneByName(zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var fqdn = config.appFqdn(subdomain);
|
||||
var params = {
|
||||
ChangeBatch: {
|
||||
Changes: [{
|
||||
Action: 'UPSERT',
|
||||
ResourceRecordSet: {
|
||||
Type: type,
|
||||
Name: fqdn,
|
||||
ResourceRecords: [{
|
||||
Value: value
|
||||
}],
|
||||
Weight: 0,
|
||||
SetIdentifier: fqdn,
|
||||
TTL: 1
|
||||
}
|
||||
}]
|
||||
},
|
||||
HostedZoneId: zone.Id
|
||||
};
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(credentials);
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.code === 'PriorRequestNotComplete') {
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
|
||||
} else if (error) {
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
debug('addSubdomain: success. changeInfoId:%j', result);
|
||||
|
||||
callback(null, result.ChangeInfo.Id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('delSubdomain: %s for domain %s.', subdomain, zoneName);
|
||||
|
||||
getZoneByName(zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var fqdn = config.appFqdn(subdomain);
|
||||
var resourceRecordSet = {
|
||||
Name: fqdn,
|
||||
Type: type,
|
||||
ResourceRecords: [{
|
||||
Value: value
|
||||
}],
|
||||
Weight: 0,
|
||||
SetIdentifier: fqdn,
|
||||
TTL: 1
|
||||
};
|
||||
|
||||
var params = {
|
||||
ChangeBatch: {
|
||||
Changes: [{
|
||||
Action: 'DELETE',
|
||||
ResourceRecordSet: resourceRecordSet
|
||||
}]
|
||||
},
|
||||
HostedZoneId: zone.Id
|
||||
};
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(credentials);
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
|
||||
debug('delSubdomain: resource record set not found.', error);
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error && error.code === 'NoSuchHostedZone') {
|
||||
debug('delSubdomain: hosted zone not found.', error);
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error && error.code === 'PriorRequestNotComplete') {
|
||||
debug('delSubdomain: resource is still busy', error);
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
|
||||
} else if (error && error.code === 'InvalidChangeBatch') {
|
||||
debug('delSubdomain: invalid change batch. No such record to be deleted.');
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error) {
|
||||
debug('delSubdomain: error', error);
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
}
|
||||
|
||||
debug('delSubdomain: success');
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getChangeStatus(changeId, callback) {
|
||||
assert.strictEqual(typeof changeId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (changeId === '') return callback(null, 'INSYNC');
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(credentials);
|
||||
route53.getChange({ Id: changeId }, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result.ChangeInfo.Status);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -12,10 +12,11 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
aws = require('./aws.js'),
|
||||
caas = require('./storage/caas.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:backups'),
|
||||
superagent = require('superagent'),
|
||||
s3 = require('./storage/s3.js'),
|
||||
settings = require('./settings.js'),
|
||||
util = require('util');
|
||||
|
||||
function BackupsError(reason, errorOrMessage) {
|
||||
@@ -39,21 +40,30 @@ function BackupsError(reason, errorOrMessage) {
|
||||
util.inherits(BackupsError, Error);
|
||||
BackupsError.EXTERNAL_ERROR = 'external error';
|
||||
BackupsError.INTERNAL_ERROR = 'internal error';
|
||||
BackupsError.MISSING_CREDENTIALS = 'missing credentials';
|
||||
|
||||
// choose which storage backend we use for test purpose we use s3
|
||||
function api(provider) {
|
||||
switch (provider) {
|
||||
case 'caas': return caas;
|
||||
case 's3': return s3;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getAllPaged(page, perPage, callback) {
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backups';
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
superagent.get(url).query({ token: config.token() }).end(function (error, result) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 200) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
|
||||
if (!result.body || !util.isArray(result.body.backups)) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response'));
|
||||
api(backupConfig.provider).getAllPaged(backupConfig, page, perPage, function (error, backups) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
|
||||
// [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first)
|
||||
return callback(null, result.body.backups);
|
||||
return callback(null, backups); // [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,19 +78,23 @@ function getBackupUrl(app, callback) {
|
||||
filename = util.format('backup_%s-v%s.tar.gz', (new Date()).toISOString(), config.version());
|
||||
}
|
||||
|
||||
aws.getSignedUploadUrl(filename, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
var obj = {
|
||||
id: filename,
|
||||
url: result.url,
|
||||
sessionToken: result.sessionToken,
|
||||
backupKey: config.backupKey()
|
||||
};
|
||||
api(backupConfig.provider).getSignedUploadUrl(backupConfig, filename, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
|
||||
var obj = {
|
||||
id: filename,
|
||||
url: result.url,
|
||||
sessionToken: result.sessionToken,
|
||||
backupKey: backupConfig.key
|
||||
};
|
||||
|
||||
callback(null, obj);
|
||||
debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
|
||||
|
||||
callback(null, obj);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,19 +103,23 @@ function getRestoreUrl(backupId, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
aws.getSignedDownloadUrl(backupId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
var obj = {
|
||||
id: backupId,
|
||||
url: result.url,
|
||||
sessionToken: result.sessionToken,
|
||||
backupKey: config.backupKey()
|
||||
};
|
||||
api(backupConfig.provider).getSignedDownloadUrl(backupConfig, backupId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('getRestoreUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
|
||||
var obj = {
|
||||
id: backupId,
|
||||
url: result.url,
|
||||
sessionToken: result.sessionToken,
|
||||
backupKey: backupConfig.key
|
||||
};
|
||||
|
||||
callback(null, obj);
|
||||
debug('getRestoreUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
|
||||
|
||||
callback(null, obj);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,9 +129,14 @@ function copyLastBackup(app, callback) {
|
||||
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);
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilename, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, toFilename);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
addSubdomain: addSubdomain,
|
||||
delSubdomain: delSubdomain,
|
||||
getChangeStatus: getChangeStatus
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:caas'),
|
||||
SubdomainError = require('./subdomainerror.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
function addSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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,
|
||||
value: value
|
||||
};
|
||||
|
||||
superagent
|
||||
.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);
|
||||
});
|
||||
}
|
||||
|
||||
function delSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('delSubdomain: %s for domain %s.', subdomain, zoneName);
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
value: value
|
||||
};
|
||||
|
||||
superagent
|
||||
.del(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain))
|
||||
.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 === 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);
|
||||
});
|
||||
}
|
||||
|
||||
function getChangeStatus(changeId, callback) {
|
||||
assert.strictEqual(typeof changeId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (changeId === '') return callback(null, 'INSYNC');
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/domains/' + config.fqdn() + '/status/' + changeId)
|
||||
.query({ token: config.token() })
|
||||
.end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.status !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
return callback(null, result.body.status);
|
||||
});
|
||||
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('../config.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
ursa = require('ursa'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
|
||||
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
|
||||
CA_ORIGIN = CA_PROD,
|
||||
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate
|
||||
};
|
||||
|
||||
function AcmeError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(AcmeError, Error);
|
||||
AcmeError.INTERNAL_ERROR = 'Internal Error';
|
||||
AcmeError.EXTERNAL_ERROR = 'External Error';
|
||||
AcmeError.ALREADY_EXISTS = 'Already Exists';
|
||||
AcmeError.NOT_COMPLETED = 'Not Completed';
|
||||
AcmeError.FORBIDDEN = 'Forbidden';
|
||||
|
||||
// http://jose.readthedocs.org/en/latest/
|
||||
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
|
||||
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
|
||||
|
||||
function getNonce(callback) {
|
||||
superagent.get(CA_ORIGIN + '/directory', function (error, response) {
|
||||
if (error) return callback(error);
|
||||
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
|
||||
|
||||
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
|
||||
});
|
||||
}
|
||||
|
||||
// urlsafe base64 encoding (jose)
|
||||
function urlBase64Encode(string) {
|
||||
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
}
|
||||
|
||||
function b64(str) {
|
||||
var buf = util.isBuffer(str) ? str : new Buffer(str);
|
||||
return urlBase64Encode(buf.toString('base64'));
|
||||
}
|
||||
|
||||
function sendSignedRequest(url, accountKeyPem, payload, callback) {
|
||||
assert.strictEqual(typeof url, 'string');
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof payload, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var privateKey = ursa.createPrivateKey(accountKeyPem);
|
||||
|
||||
var header = {
|
||||
alg: 'RS256',
|
||||
jwk: {
|
||||
e: b64(privateKey.getExponent()),
|
||||
kty: 'RSA',
|
||||
n: b64(privateKey.getModulus())
|
||||
}
|
||||
};
|
||||
|
||||
var payload64 = b64(payload);
|
||||
|
||||
getNonce(function (error, nonce) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
|
||||
|
||||
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
|
||||
|
||||
var signer = ursa.createSigner('sha256');
|
||||
signer.update(protected64 + '.' + payload64, 'utf8');
|
||||
var signature64 = urlBase64Encode(signer.sign(privateKey, 'base64'));
|
||||
|
||||
var data = {
|
||||
header: header,
|
||||
protected: protected64,
|
||||
payload: payload64,
|
||||
signature: signature64
|
||||
};
|
||||
|
||||
superagent.post(url).set('Content-Type', 'application/x-www-form-urlencoded').send(JSON.stringify(data)).end(function (error, res) {
|
||||
if (error && !error.response) return callback(error); // network errors
|
||||
|
||||
callback(null, res);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function registerUser(accountKeyPem, email, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
resource: 'new-reg',
|
||||
contact: [ 'mailto:' + email ],
|
||||
agreement: LE_AGREEMENT
|
||||
};
|
||||
|
||||
debug('registerUser: %s', email);
|
||||
|
||||
sendSignedRequest(CA_ORIGIN + '/acme/new-reg', accountKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode === 409) return callback(new AcmeError(AcmeError.ALREADY_EXISTS, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('registerUser: registered user %s', email);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function registerDomain(accountKeyPem, domain, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
resource: 'new-authz',
|
||||
identifier: {
|
||||
type: 'dns',
|
||||
value: domain
|
||||
}
|
||||
};
|
||||
|
||||
debug('registerDomain: %s', domain);
|
||||
|
||||
sendSignedRequest(CA_ORIGIN + '/acme/new-authz', accountKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new AcmeError(AcmeError.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('registerDomain: registered %s', domain);
|
||||
|
||||
callback(null, result.body);
|
||||
});
|
||||
}
|
||||
|
||||
function prepareHttpChallenge(accountKeyPem, challenge, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
|
||||
|
||||
var token = challenge.token;
|
||||
|
||||
var privateKey = ursa.createPrivateKey(accountKeyPem);
|
||||
|
||||
var jwk = {
|
||||
e: b64(privateKey.getExponent()),
|
||||
kty: 'RSA',
|
||||
n: b64(privateKey.getModulus())
|
||||
};
|
||||
|
||||
var shasum = crypto.createHash('sha256');
|
||||
shasum.update(JSON.stringify(jwk));
|
||||
var thumbprint = urlBase64Encode(shasum.digest('base64'));
|
||||
var keyAuthorization = token + '.' + thumbprint;
|
||||
|
||||
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, token));
|
||||
|
||||
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, token), token + '.' + thumbprint, function (error) {
|
||||
if (error) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function notifyChallengeReady(accountKeyPem, challenge, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('notifyChallengeReady: %s was met', challenge.uri);
|
||||
|
||||
var keyAuthorization = fs.readFileSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), 'utf8');
|
||||
|
||||
var payload = {
|
||||
resource: 'challenge',
|
||||
keyAuthorization: keyAuthorization
|
||||
};
|
||||
|
||||
sendSignedRequest(challenge.uri, accountKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function waitForChallenge(challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('waitingForChallenge: %j', challenge);
|
||||
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
debug('waitingForChallenge: getting status');
|
||||
|
||||
superagent.get(challenge.uri).end(function (error, result) {
|
||||
if (error && !error.response) {
|
||||
debug('waitForChallenge: network error getting uri %s', challenge.uri);
|
||||
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, error.message)); // network error
|
||||
}
|
||||
if (result.statusCode !== 202) {
|
||||
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForChallenge: status is "%s"', result.body.status);
|
||||
|
||||
if (result.body.status === 'pending') return retryCallback(new AcmeError(AcmeError.NOT_COMPLETED));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
else return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
});
|
||||
}, function retryFinished(error) {
|
||||
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
|
||||
function signCertificate(accountKeyPem, domain, csrDer, callback) {
|
||||
assert(util.isBuffer(accountKeyPem));
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(util.isBuffer(csrDer));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
|
||||
var payload = {
|
||||
resource: 'new-cert',
|
||||
csr: b64(csrDer)
|
||||
};
|
||||
|
||||
debug('signCertificate: sending new-cert request');
|
||||
|
||||
sendSignedRequest(CA_ORIGIN + '/acme/new-cert', accountKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||
// 429 means we reached the cert limit for this domain
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var certUrl = result.headers.location;
|
||||
|
||||
if (!certUrl) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
|
||||
|
||||
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // for renewal
|
||||
|
||||
return callback(null, result.headers.location);
|
||||
});
|
||||
}
|
||||
|
||||
function createKeyAndCsr(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
var execSync = safe.child_process.execSync;
|
||||
|
||||
var privateKeyFile = path.join(outdir, domain + '.key');
|
||||
var key = execSync('openssl genrsa 4096');
|
||||
if (!key) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
|
||||
|
||||
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
|
||||
if (!csrDer) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
var csrFile = path.join(outdir, domain + '.csr');
|
||||
if (!safe.fs.writeFileSync(csrFile, csrFile)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
|
||||
|
||||
callback(null, csrDer);
|
||||
}
|
||||
|
||||
function downloadCertificate(domain, certUrl, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof certUrl, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
|
||||
superagent.get(certUrl).buffer().parse(function (res, done) {
|
||||
var data = [ ];
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
}).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode === 202) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, 'Retry not implemented yet'));
|
||||
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var certificateDer = result.text;
|
||||
var execSync = safe.child_process.execSync;
|
||||
|
||||
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
|
||||
debug('downloadCertificate: cert der file saved');
|
||||
|
||||
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
|
||||
if (!certificatePem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
var chainPem = safe.fs.readFileSync(__dirname + '/lets-encrypt-x1-cross-signed.pem.txt');
|
||||
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
var certificateFile = path.join(outdir, domain + '.cert');
|
||||
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('downloadCertificate: cert file saved at %s', certificateFile);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function acmeFlow(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
||||
// we cannot use admin@fqdn because the user might not have set it up.
|
||||
// we cannot use owner email because we don't have it yet (the admin cert is fetched before activation)
|
||||
// one option is to update the owner email when a second cert is requested (https://github.com/ietf-wg-acme/acme/issues/30)
|
||||
var email = 'admin@cloudron.io';
|
||||
var accountKeyPem;
|
||||
|
||||
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
|
||||
debug('getCertificate: generating acme account key on first run');
|
||||
accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!accountKeyPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, accountKeyPem);
|
||||
} else {
|
||||
debug('getCertificate: using existing acme account key');
|
||||
accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
|
||||
}
|
||||
|
||||
registerUser(accountKeyPem, email, function (error) {
|
||||
if (error && error.reason !== AcmeError.ALREADY_EXISTS) return callback(error);
|
||||
|
||||
registerDomain(accountKeyPem, domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('acmeFlow: challenges: %j', result);
|
||||
|
||||
var httpChallenges = result.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
if (httpChallenges.length === 0) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'no http challenges'));
|
||||
var challenge = httpChallenges[0];
|
||||
|
||||
async.waterfall([
|
||||
prepareHttpChallenge.bind(null, accountKeyPem, challenge),
|
||||
notifyChallengeReady.bind(null, accountKeyPem, challenge),
|
||||
waitForChallenge.bind(null, challenge),
|
||||
createKeyAndCsr.bind(null, domain),
|
||||
signCertificate.bind(null, accountKeyPem, domain),
|
||||
downloadCertificate.bind(null, domain)
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getCertificate(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
var certUrl = safe.fs.readFileSync(path.join(outdir, domain + '.url'), 'utf8');
|
||||
var certificateGetter;
|
||||
if (certUrl) {
|
||||
debug('getCertificate: renewing existing cert for %s from %s', domain, certUrl);
|
||||
certificateGetter = downloadCertificate.bind(null, domain, certUrl);
|
||||
} else {
|
||||
debug('getCertificate: start acme flow for %s', domain);
|
||||
certificateGetter = acmeFlow.bind(null, domain);
|
||||
}
|
||||
|
||||
certificateGetter(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:cert/caas.js');
|
||||
|
||||
function getCertificate(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getCertificate: using fallback certificate', domain);
|
||||
|
||||
return callback(null, 'cert/host.cert', 'cert/host.key');
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqDCCA5CgAwIBAgIRAJgT9HUT5XULQ+dDHpceRL0wDQYJKoZIhvcNAQELBQAw
|
||||
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
|
||||
Ew5EU1QgUm9vdCBDQSBYMzAeFw0xNTEwMTkyMjMzMzZaFw0yMDEwMTkyMjMzMzZa
|
||||
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
|
||||
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||
ggEPADCCAQoCggEBAJzTDPBa5S5Ht3JdN4OzaGMw6tc1Jhkl4b2+NfFwki+3uEtB
|
||||
BaupnjUIWOyxKsRohwuj43Xk5vOnYnG6eYFgH9eRmp/z0HhncchpDpWRz/7mmelg
|
||||
PEjMfspNdxIknUcbWuu57B43ABycrHunBerOSuu9QeU2mLnL/W08lmjfIypCkAyG
|
||||
dGfIf6WauFJhFBM/ZemCh8vb+g5W9oaJ84U/l4avsNwa72sNlRZ9xCugZbKZBDZ1
|
||||
gGusSvMbkEl4L6KWTyogJSkExnTA0DHNjzE4lRa6qDO4Q/GxH8Mwf6J5MRM9LTb4
|
||||
4/zyM2q5OTHFr8SNDR1kFjOq+oQpttQLwNh9w5MCAwEAAaOCAZIwggGOMBIGA1Ud
|
||||
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAy
|
||||
BggrBgEFBQcwAYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5j
|
||||
b20wOwYIKwYBBQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMv
|
||||
ZHN0cm9vdGNheDMucDdjMB8GA1UdIwQYMBaAFMSnsaR7LHH62+FLkHX/xBVghYkQ
|
||||
MFQGA1UdIARNMEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUH
|
||||
AgEWImh0dHA6Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUw
|
||||
MzAxoC+gLYYraHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JM
|
||||
LmNybDATBgNVHR4EDDAKoQgwBoIELm1pbDAdBgNVHQ4EFgQUqEpqYwR93brm0Tm3
|
||||
pkVl7/Oo7KEwDQYJKoZIhvcNAQELBQADggEBANHIIkus7+MJiZZQsY14cCoBG1hd
|
||||
v0J20/FyWo5ppnfjL78S2k4s2GLRJ7iD9ZDKErndvbNFGcsW+9kKK/TnY21hp4Dd
|
||||
ITv8S9ZYQ7oaoqs7HwhEMY9sibED4aXw09xrJZTC9zK1uIfW6t5dHQjuOWv+HHoW
|
||||
ZnupyxpsEUlEaFb+/SCI4KCSBdAsYxAcsHYI5xxEI4LutHp6s3OT2FuO90WfdsIk
|
||||
6q78OMSdn875bNjdBYAqxUp2/LEIHfDBkLoQz0hFJmwAbYahqKaLn73PAAm1X2kj
|
||||
f1w8DdnkabOLGeOVcj9LQ+s67vBykx4anTjURkbqZslUEUsn2k5xeua2zUk=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,241 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var acme = require('./cert/acme.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
caas = require('./cert/caas.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:src/certificates'),
|
||||
fs = require('fs'),
|
||||
nginx = require('./nginx.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js'),
|
||||
x509 = require('x509');
|
||||
|
||||
exports = module.exports = {
|
||||
installAdminCertificate: installAdminCertificate,
|
||||
autoRenew: autoRenew,
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
setAdminCertificate: setAdminCertificate,
|
||||
CertificatesError: CertificatesError,
|
||||
validateCertificate: validateCertificate,
|
||||
ensureCertificate: ensureCertificate
|
||||
};
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function CertificatesError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(CertificatesError, Error);
|
||||
CertificatesError.INTERNAL_ERROR = 'Internal Error';
|
||||
CertificatesError.INVALID_CERT = 'Invalid certificate';
|
||||
|
||||
function getApi(callback) {
|
||||
settings.getTlsConfig(function (error, tlsConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var api = tlsConfig.provider === 'caas' ? caas : acme;
|
||||
|
||||
callback(null, api);
|
||||
});
|
||||
}
|
||||
|
||||
function installAdminCertificate(callback) {
|
||||
if (cloudron.isConfiguredSync()) return callback();
|
||||
|
||||
settings.getTlsConfig(function (error, tlsConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (tlsConfig.provider === 'caas') return callback();
|
||||
|
||||
waitForDns(config.adminFqdn(), sysinfo.getIp(), config.fqdn(), function (error) {
|
||||
if (error) return callback(error); // this cannot happen because we retry forever
|
||||
|
||||
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
|
||||
if (error) { // currently, this can never happen
|
||||
debug('Error obtaining certificate. Proceed anyway', error);
|
||||
return callback();
|
||||
}
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function needsRenewalSync(certFilePath) {
|
||||
var result = safe.child_process.execSync('openssl x509 -checkend %s -in %s', 60 * 60 * 24 * 5, certFilePath);
|
||||
|
||||
return result === null; // command errored
|
||||
}
|
||||
|
||||
function autoRenew(callback) {
|
||||
debug('autoRenew: Checking certificates for renewal');
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var filenames = safe.fs.readdirSync(paths.APP_CERTS_DIR);
|
||||
if (!filenames) {
|
||||
debug('autoRenew: Error getting filenames: %s', safe.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
var certs = filenames.filter(function (f) {
|
||||
return f.match(/\.cert$/) !== null && needsRenewalSync(path.join(paths.APP_CERTS_DIR, f));
|
||||
});
|
||||
|
||||
debug('autoRenew: %j needs to be renewed', certs);
|
||||
|
||||
getApi(function (error, api) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(certs, function iterator(cert, iteratorCallback) {
|
||||
var domain = cert.match(/^(.*)\.cert$/)[1];
|
||||
if (domain === 'host') return iteratorCallback(); // cannot renew fallback cert
|
||||
|
||||
api.getCertificate(domain, function (error) {
|
||||
if (error) debug('autoRenew: could not renew cert for %s', domain, error);
|
||||
|
||||
iteratorCallback(); // move on to next cert
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
|
||||
// servers certificate appears first (and not the intermediate cert)
|
||||
function validateCertificate(cert, key, fqdn) {
|
||||
assert(cert === null || typeof cert === 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
|
||||
if (cert === null && key === null) return null;
|
||||
if (!cert && key) return new Error('missing cert');
|
||||
if (cert && !key) return new Error('missing key');
|
||||
|
||||
var content;
|
||||
try {
|
||||
content = x509.parseCert(cert);
|
||||
} catch (e) {
|
||||
return new Error('invalid cert: ' + e.message);
|
||||
}
|
||||
|
||||
// check expiration
|
||||
if (content.notAfter < new Date()) return new Error('cert expired');
|
||||
|
||||
function matchesDomain(domain) {
|
||||
if (domain === fqdn) return true;
|
||||
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// check domain
|
||||
var domains = content.altNames.concat(content.subject.commonName);
|
||||
if (!domains.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, domains));
|
||||
|
||||
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
|
||||
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
|
||||
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
|
||||
if (certModulus !== keyModulus) return new Error('key does not match the cert');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function setFallbackCertificate(cert, key, callback) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateCertificate(cert, key, '*.' + config.fqdn());
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
|
||||
|
||||
// backup the cert
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
// copy over fallback cert
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
nginx.reload(function (error) {
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function setAdminCertificate(cert, key, callback) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.appFqdn(constants.ADMIN_LOCATION);
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
|
||||
|
||||
var error = validateCertificate(cert, key, vhost);
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
|
||||
|
||||
// backup the cert
|
||||
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
||||
}
|
||||
|
||||
function ensureCertificate(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// check if user uploaded a specific cert. ideally, we should not mix user certs and automatic certs as we do here...
|
||||
var userCertFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
||||
var userKeyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
||||
|
||||
if (fs.existsSync(userCertFilePath) && fs.existsSync(userKeyFilePath)) {
|
||||
debug('ensureCertificate: %s. certificate already exists at %s', domain, userKeyFilePath);
|
||||
|
||||
if (!needsRenewalSync(userCertFilePath)) return callback(null, userCertFilePath, userKeyFilePath);
|
||||
|
||||
debug('ensureCertificate: %s cert require renewal', domain);
|
||||
}
|
||||
|
||||
getApi(function (error, api) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('ensureCertificate: getting certificate for %s', domain);
|
||||
|
||||
api.getCertificate(domain, function (error, certFilePath, keyFilePath) {
|
||||
if (error) {
|
||||
debug('ensureCertificate: could not get certificate. using fallback certs', error);
|
||||
return callback(null, 'cert/host.cert', 'cert/host.key'); // use fallback certs
|
||||
}
|
||||
|
||||
callback(null, certFilePath, keyFilePath);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -11,15 +11,20 @@ exports = module.exports = {
|
||||
getConfig: getConfig,
|
||||
getStatus: getStatus,
|
||||
|
||||
setCertificate: setCertificate,
|
||||
|
||||
sendHeartbeat: sendHeartbeat,
|
||||
|
||||
update: update,
|
||||
reboot: reboot,
|
||||
migrate: migrate,
|
||||
backup: backup,
|
||||
ensureBackup: ensureBackup
|
||||
ensureBackup: ensureBackup,
|
||||
|
||||
isConfiguredSync: isConfiguredSync,
|
||||
|
||||
events: new (require('events').EventEmitter)(),
|
||||
|
||||
EVENT_ACTIVATED: 'activated',
|
||||
EVENT_CONFIGURED: 'configured'
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
@@ -28,6 +33,7 @@ var apps = require('./apps.js'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
bytes = require('bytes'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
@@ -50,14 +56,16 @@ var apps = require('./apps.js'),
|
||||
util = require('util'),
|
||||
webhooks = require('./webhooks.js');
|
||||
|
||||
var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
|
||||
REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
|
||||
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
|
||||
BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
|
||||
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
|
||||
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update';
|
||||
|
||||
var gAddDnsRecordsTimerId = null,
|
||||
gCloudronDetails = null; // cached cloudron details like region,size...
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
var gUpdatingDns = false, // flag for dns update reentrancy
|
||||
gCloudronDetails = null, // cached cloudron details like region,size...
|
||||
gIsConfigured = null; // cached configured state so that return value is synchronous. null means we are not initialized yet
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(!app || typeof app === 'object');
|
||||
@@ -75,7 +83,6 @@ function ignoreError(func) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function CloudronError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
@@ -109,22 +116,61 @@ CloudronError.NOT_FOUND = 'Not found';
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (process.env.BOX_ENV !== 'test') {
|
||||
addDnsRecords();
|
||||
}
|
||||
exports.events.on(exports.EVENT_CONFIGURED, addDnsRecords);
|
||||
|
||||
callback(null);
|
||||
syncConfigState(callback);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clearTimeout(gAddDnsRecordsTimerId);
|
||||
gAddDnsRecordsTimerId = null;
|
||||
exports.events.removeListener(exports.EVENT_CONFIGURED, addDnsRecords);
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function isConfiguredSync() {
|
||||
return gIsConfigured === true;
|
||||
}
|
||||
|
||||
function isConfigured(callback) {
|
||||
// set of rules to see if we have the configs required for cloudron to function
|
||||
// note this checks for missing configs and not invalid configs
|
||||
|
||||
settings.getDnsConfig(function (error, dnsConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!dnsConfig) return callback(null, false);
|
||||
|
||||
var isConfigured = (config.isCustomDomain() && dnsConfig.provider === 'route53') ||
|
||||
(!config.isCustomDomain() && dnsConfig.provider === 'caas');
|
||||
|
||||
callback(null, isConfigured);
|
||||
});
|
||||
}
|
||||
|
||||
function syncConfigState(callback) {
|
||||
assert(!gIsConfigured);
|
||||
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
isConfigured(function (error, configured) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('syncConfigState: configured = %s', configured);
|
||||
|
||||
if (configured) {
|
||||
exports.events.emit(exports.EVENT_CONFIGURED);
|
||||
} else {
|
||||
settings.events.once(settings.DNS_CONFIG_KEY, function () { syncConfigState(); }); // check again later
|
||||
}
|
||||
|
||||
gIsConfigured = configured;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function setTimeZone(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -132,7 +178,7 @@ function setTimeZone(ip, callback) {
|
||||
debug('setTimeZone ip:%s', ip);
|
||||
|
||||
superagent.get('http://www.telize.com/geoip/' + ip).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
if ((error && !error.response) || result.statusCode !== 200) {
|
||||
debug('Failed to get geo location', error);
|
||||
return callback(null);
|
||||
}
|
||||
@@ -176,6 +222,9 @@ function activate(username, password, email, ip, callback) {
|
||||
tokendb.add(token, tokendb.PREFIX_USER + userObject.id, result.id, expires, '*', function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
// EE API is sync. do not keep the REST API reponse waiting
|
||||
process.nextTick(function () { exports.events.emit(exports.EVENT_ACTIVATED); });
|
||||
|
||||
callback(null, { token: token, expires: expires });
|
||||
});
|
||||
});
|
||||
@@ -194,6 +243,8 @@ function getStatus(callback) {
|
||||
callback(null, {
|
||||
activated: count !== 0,
|
||||
version: config.version(),
|
||||
boxVersionsUrl: config.get('boxVersionsUrl'),
|
||||
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
|
||||
cloudronName: cloudronName
|
||||
});
|
||||
});
|
||||
@@ -209,8 +260,8 @@ function getCloudronDetails(callback) {
|
||||
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
|
||||
.query({ token: config.token() })
|
||||
.end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.status !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
gCloudronDetails = result.body.box;
|
||||
|
||||
@@ -221,10 +272,9 @@ function getCloudronDetails(callback) {
|
||||
function getConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// TODO avoid pyramid of awesomeness with async
|
||||
getCloudronDetails(function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to fetch cloudron details.', error);
|
||||
debug('Failed to fetch cloudron details.', error);
|
||||
|
||||
// set fallback values to avoid dependency on appstore
|
||||
result = {
|
||||
@@ -233,6 +283,10 @@ function getConfig(callback) {
|
||||
};
|
||||
}
|
||||
|
||||
// We rely at the moment on the size being specified in 512mb,1gb,...
|
||||
// TODO provide that number from the appstore
|
||||
var memory = bytes(result.size) || 0;
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -252,6 +306,7 @@ function getConfig(callback) {
|
||||
developerMode: developerMode,
|
||||
region: result.region,
|
||||
size: result.size,
|
||||
memory: memory,
|
||||
cloudronName: cloudronName
|
||||
});
|
||||
});
|
||||
@@ -260,104 +315,119 @@ function getConfig(callback) {
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
// Only send heartbeats after the admin dns record is synced to give appstore a chance to know that fact
|
||||
if (!config.get('dnsInSync')) return;
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
||||
|
||||
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
|
||||
if (error) debug('Error sending heartbeat.', error);
|
||||
if (error && !error.response) debug('Network error sending heartbeat.', error);
|
||||
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
|
||||
else debug('Heartbeat sent to %s', url);
|
||||
});
|
||||
}
|
||||
|
||||
function addDnsRecords() {
|
||||
if (config.get('dnsInSync')) return sendHeartbeat(); // already registered send heartbeat
|
||||
|
||||
var DKIM_SELECTOR = 'mail';
|
||||
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
|
||||
|
||||
function readDkimPublicKeySync() {
|
||||
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
|
||||
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
|
||||
|
||||
if (publicKey === null) {
|
||||
console.error('Error reading dkim public key. Stop DNS setup.');
|
||||
return;
|
||||
debug('Error reading dkim public key.', safe.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// remove header, footer and new lines
|
||||
publicKey = publicKey.split('\n').slice(1, -2).join('');
|
||||
|
||||
// note that dmarc requires special DNS records for external RUF and RUA
|
||||
var records = [
|
||||
// naked domain
|
||||
{ subdomain: '', type: 'A', value: sysinfo.getIp() },
|
||||
// webadmin domain
|
||||
{ subdomain: 'my', type: 'A', value: sysinfo.getIp() },
|
||||
// softfail all mails not from our IP. Note that this uses IP instead of 'a' should we use a load balancer in the future
|
||||
{ subdomain: '', type: 'TXT', value: '"v=spf1 ip4:' + sysinfo.getIp() + ' ~all"' },
|
||||
// t=s limits the domainkey to this domain and not it's subdomains
|
||||
{ subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', value: '"v=DKIM1; t=s; p=' + publicKey + '"' },
|
||||
// DMARC requires special setup if report email id is in different domain
|
||||
{ subdomain: '_dmarc', type: 'TXT', value: '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' }
|
||||
];
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
debug('addDnsRecords:', records);
|
||||
function txtRecordsWithSpf(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomains.addMany(records, function (error, changeIds) {
|
||||
if (error) {
|
||||
console.error('Admin DNS record addition failed', error);
|
||||
gAddDnsRecordsTimerId = setTimeout(addDnsRecords, 10000);
|
||||
return;
|
||||
subdomains.get('', 'TXT', function (error, txtRecords) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
|
||||
|
||||
var i, validSpf;
|
||||
|
||||
for (i = 0; i < txtRecords.length; i++) {
|
||||
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
|
||||
|
||||
validSpf = txtRecords[i].indexOf(' a:' + config.fqdn() + ' ') !== -1;
|
||||
break;
|
||||
}
|
||||
|
||||
function checkIfInSync() {
|
||||
debug('addDnsRecords: Check if admin DNS record is in sync.');
|
||||
if (validSpf) return callback(null, null);
|
||||
|
||||
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') return callback(new Error(changeId + ' is not in sync. result:' + result));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
debug(error.message);
|
||||
gAddDnsRecordsTimerId = setTimeout(checkIfInSync, 5000);
|
||||
return;
|
||||
}
|
||||
debug('addDnsRecords: done');
|
||||
config.set('dnsInSync', true);
|
||||
sendHeartbeat(); // send heartbeat after the dns records are done
|
||||
});
|
||||
if (i == txtRecords.length) {
|
||||
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + ' ~all"';
|
||||
} else {
|
||||
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + ' ' + txtRecords[i].slice('"v=spf1 '.length);
|
||||
}
|
||||
|
||||
checkIfInSync();
|
||||
return callback(null, txtRecords);
|
||||
});
|
||||
}
|
||||
|
||||
function setCertificate(certificate, key, callback) {
|
||||
assert.strictEqual(typeof certificate, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
function addDnsRecords() {
|
||||
var callback = NOOP_CALLBACK;
|
||||
|
||||
debug('Updating certificates');
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), certificate)) {
|
||||
return callback(new CloudronError(CloudronError.INTERNAL_ERROR, safe.error.message));
|
||||
if (gUpdatingDns) {
|
||||
debug('addDnsRecords: dns update already in progress');
|
||||
return callback();
|
||||
}
|
||||
gUpdatingDns = true;
|
||||
|
||||
var DKIM_SELECTOR = 'cloudron';
|
||||
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
|
||||
|
||||
var dkimKey = readDkimPublicKeySync();
|
||||
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('internal error failed to read dkim public key')));
|
||||
|
||||
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ sysinfo.getIp() ] };
|
||||
var webadminRecord = { subdomain: 'my', type: 'A', values: [ sysinfo.getIp() ] };
|
||||
// t=s limits the domainkey to this domain and not it's subdomains
|
||||
var dkimRecord = { subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||
// DMARC requires special setup if report email id is in different domain
|
||||
var dmarcRecord = { subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' ] };
|
||||
|
||||
var records = [ ];
|
||||
if (config.isCustomDomain()) {
|
||||
records.push(webadminRecord);
|
||||
records.push(dkimRecord);
|
||||
} else {
|
||||
records.push(nakedDomainRecord);
|
||||
records.push(webadminRecord);
|
||||
records.push(dkimRecord);
|
||||
records.push(dmarcRecord);
|
||||
}
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) {
|
||||
return callback(new CloudronError(CloudronError.INTERNAL_ERROR, safe.error.message));
|
||||
}
|
||||
debug('addDnsRecords: %j', records);
|
||||
|
||||
shell.sudo('setCertificate', [ RELOAD_NGINX_CMD ], function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
|
||||
txtRecordsWithSpf(function (error, txtRecords) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
return callback(null);
|
||||
if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords });
|
||||
|
||||
debug('addDnsRecords: will update %j', records);
|
||||
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
subdomains.update(record.subdomain, record.type, record.values, iteratorCallback);
|
||||
}, function (error, changeIds) {
|
||||
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
|
||||
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
gUpdatingDns = false;
|
||||
|
||||
debug('addDnsRecords: done updating records with error:', error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -396,10 +466,10 @@ function migrate(size, region, callback) {
|
||||
.query({ token: config.token() })
|
||||
.send({ size: size, region: region, restoreKey: restoreKey })
|
||||
.end(function (error, result) {
|
||||
if (error) return unlock(error);
|
||||
if (result.status === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
|
||||
if (result.status === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
|
||||
if (result.status !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
if (error && !error.response) return unlock(error);
|
||||
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
|
||||
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
|
||||
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
return unlock(null);
|
||||
});
|
||||
@@ -422,7 +492,7 @@ function update(boxUpdateInfo, callback) {
|
||||
debug('Starting upgrade');
|
||||
doUpgrade(boxUpdateInfo, function (error) {
|
||||
if (error) {
|
||||
debug('Upgrade failed with error: %s', error);
|
||||
console.error('Upgrade failed with error:', error);
|
||||
locker.unlock(locker.OP_BOX_UPDATE);
|
||||
}
|
||||
});
|
||||
@@ -430,7 +500,7 @@ function update(boxUpdateInfo, callback) {
|
||||
debug('Starting update');
|
||||
doUpdate(boxUpdateInfo, function (error) {
|
||||
if (error) {
|
||||
debug('Update failed with error: %s', error);
|
||||
console.error('Update failed with error:', error);
|
||||
locker.unlock(locker.OP_BOX_UPDATE);
|
||||
}
|
||||
});
|
||||
@@ -456,8 +526,8 @@ function doUpgrade(boxUpdateInfo, callback) {
|
||||
.query({ token: config.token() })
|
||||
.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(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
|
||||
if (result.statusCode !== 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');
|
||||
|
||||
@@ -485,8 +555,8 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/sourcetarballurl')
|
||||
.query({ token: config.token(), boxVersion: boxUpdateInfo.version })
|
||||
.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 (error && !error.response) return updateError(new Error('Network error fetching sourceTarballUrl: ' + error));
|
||||
if (result.statusCode !== 200) return updateError(new Error('Error fetching sourceTarballUrl status: ' + result.statusCode));
|
||||
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
|
||||
@@ -495,27 +565,34 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
|
||||
// this data is opaque to the installer
|
||||
data: {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
aws: config.aws(),
|
||||
backupKey: config.backupKey(),
|
||||
boxVersionsUrl: config.get('boxVersionsUrl'),
|
||||
fqdn: config.fqdn(),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
restoreUrl: null,
|
||||
restoreKey: null,
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
fqdn: config.fqdn(),
|
||||
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
|
||||
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
|
||||
appstore: {
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin()
|
||||
},
|
||||
caas: {
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin()
|
||||
},
|
||||
|
||||
version: boxUpdateInfo.version,
|
||||
webServerOrigin: config.webServerOrigin()
|
||||
boxVersionsUrl: config.get('boxVersionsUrl')
|
||||
}
|
||||
};
|
||||
|
||||
debug('updating box %j', args);
|
||||
|
||||
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: ' + JSON.stringify(result.body)));
|
||||
if (error && !error.response) return updateError(error);
|
||||
if (result.statusCode !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
|
||||
|
||||
@@ -618,13 +695,13 @@ function backupBoxAndApps(callback) {
|
||||
++processed;
|
||||
|
||||
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, 'Unable to backup', error);
|
||||
return iteratorCallback(error);
|
||||
}
|
||||
|
||||
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
|
||||
|
||||
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
|
||||
});
|
||||
}, function appsBackedUp(error, backupIds) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
LoadPlugin "table"
|
||||
<Plugin table>
|
||||
<Table "/sys/fs/cgroup/memory/system.slice/docker-<%= containerId %>.scope/memory.stat">
|
||||
<Table "/sys/fs/cgroup/memory/system.slice/docker.service/docker/<%= containerId %>/memory.stat">
|
||||
Instance "<%= appId %>-memory"
|
||||
Separator " \\n"
|
||||
<Result>
|
||||
@@ -10,7 +10,7 @@ LoadPlugin "table"
|
||||
</Result>
|
||||
</Table>
|
||||
|
||||
<Table "/sys/fs/cgroup/memory/system.slice/docker-<%= containerId %>.scope/memory.max_usage_in_bytes">
|
||||
<Table "/sys/fs/cgroup/memory/system.slice/docker.service/docker/<%= containerId %>/memory.max_usage_in_bytes">
|
||||
Instance "<%= appId %>-memory"
|
||||
Separator "\\n"
|
||||
<Result>
|
||||
@@ -20,7 +20,7 @@ LoadPlugin "table"
|
||||
</Result>
|
||||
</Table>
|
||||
|
||||
<Table "/sys/fs/cgroup/cpuacct/system.slice/docker-<%= containerId %>.scope/cpuacct.stat">
|
||||
<Table "/sys/fs/cgroup/cpuacct/docker/<%= containerId %>/cpuacct.stat">
|
||||
Instance "<%= appId %>-cpu"
|
||||
Separator " \\n"
|
||||
<Result>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
exports = module.exports = {
|
||||
baseDir: baseDir,
|
||||
dnsInSync: dnsInSync,
|
||||
setDnsInSync: setDnsInSync,
|
||||
|
||||
// values set here will be lost after a upgrade/update. use the sqlite database
|
||||
// for persistent values that need to be backed up
|
||||
@@ -26,14 +28,12 @@ exports = module.exports = {
|
||||
// these values are derived
|
||||
adminOrigin: adminOrigin,
|
||||
internalAdminOrigin: internalAdminOrigin,
|
||||
adminFqdn: adminFqdn,
|
||||
appFqdn: appFqdn,
|
||||
zoneName: zoneName,
|
||||
|
||||
isDev: isDev,
|
||||
|
||||
backupKey: backupKey,
|
||||
aws: aws,
|
||||
|
||||
// for testing resets to defaults
|
||||
_reset: initConfig
|
||||
};
|
||||
@@ -56,6 +56,14 @@ function baseDir() {
|
||||
|
||||
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
|
||||
|
||||
function dnsInSync() {
|
||||
return !!safe.fs.statSync(require('./paths.js').DNS_IN_SYNC_FILE);
|
||||
}
|
||||
|
||||
function setDnsInSync(content) {
|
||||
safe.fs.writeFileSync(require('./paths.js').DNS_IN_SYNC_FILE, content || 'if this file exists, dns is in sync');
|
||||
}
|
||||
|
||||
function saveSync() {
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
|
||||
}
|
||||
@@ -65,9 +73,7 @@ function initConfig() {
|
||||
data.fqdn = 'localhost';
|
||||
|
||||
data.token = null;
|
||||
data.mailServer = null;
|
||||
data.adminEmail = null;
|
||||
data.mailDnsRecordIds = [ ];
|
||||
data.boxVersionsUrl = null;
|
||||
data.version = null;
|
||||
data.isCustomDomain = false;
|
||||
@@ -76,14 +82,6 @@ function initConfig() {
|
||||
data.ldapPort = 3002;
|
||||
data.oauthProxyPort = 3003;
|
||||
data.simpleAuthPort = 3004;
|
||||
data.backupKey = 'backupKey';
|
||||
data.aws = {
|
||||
backupBucket: null,
|
||||
backupPrefix: null,
|
||||
accessKeyId: null, // selfhosting only
|
||||
secretAccessKey: null // selfhosting only
|
||||
};
|
||||
data.dnsInSync = false;
|
||||
|
||||
if (exports.CLOUDRON) {
|
||||
data.port = 3000;
|
||||
@@ -100,9 +98,6 @@ function initConfig() {
|
||||
name: 'boxtest'
|
||||
};
|
||||
data.token = 'APPSTORE_TOKEN';
|
||||
data.aws.backupBucket = 'testbucket';
|
||||
data.aws.backupPrefix = 'testprefix';
|
||||
data.aws.endpoint = 'http://localhost:5353';
|
||||
} else {
|
||||
assert(false, 'Unknown environment. This should not happen!');
|
||||
}
|
||||
@@ -161,6 +156,10 @@ function appFqdn(location) {
|
||||
return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn();
|
||||
}
|
||||
|
||||
function adminFqdn() {
|
||||
return appFqdn(constants.ADMIN_LOCATION);
|
||||
}
|
||||
|
||||
function adminOrigin() {
|
||||
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
|
||||
}
|
||||
@@ -195,11 +194,3 @@ function database() {
|
||||
function isDev() {
|
||||
return /dev/i.test(get('boxVersionsUrl'));
|
||||
}
|
||||
|
||||
function backupKey() {
|
||||
return get('backupKey');
|
||||
}
|
||||
|
||||
function aws() {
|
||||
return get('aws');
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ exports = module.exports = {
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
certificates = require('./certificates.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
config = require('./config.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
@@ -23,9 +24,8 @@ var gAutoupdaterJob = null,
|
||||
gBackupJob = null,
|
||||
gCleanupTokensJob = null,
|
||||
gDockerVolumeCleanerJob = null,
|
||||
gSchedulerSyncJob = null;
|
||||
|
||||
var gInitialized = false;
|
||||
gSchedulerSyncJob = null,
|
||||
gCertificateRenewJob = null;
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
|
||||
@@ -40,14 +40,19 @@ var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gInitialized) return callback();
|
||||
gHeartbeatJob = new CronJob({
|
||||
cronTime: '00 */1 * * * *', // every minute
|
||||
onTick: cloudron.sendHeartbeat,
|
||||
start: true
|
||||
});
|
||||
cloudron.sendHeartbeat(); // latest unpublished version of CronJob has runOnInit
|
||||
|
||||
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
|
||||
gInitialized = true;
|
||||
|
||||
recreateJobs(callback);
|
||||
if (cloudron.isConfiguredSync()) {
|
||||
recreateJobs(callback);
|
||||
} else {
|
||||
cloudron.events.on(cloudron.EVENT_ACTIVATED, recreateJobs);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
function recreateJobs(unusedTimeZone, callback) {
|
||||
@@ -56,14 +61,6 @@ function recreateJobs(unusedTimeZone, callback) {
|
||||
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
|
||||
onTick: cloudron.sendHeartbeat,
|
||||
start: true,
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
});
|
||||
|
||||
if (gBackupJob) gBackupJob.stop();
|
||||
gBackupJob = new CronJob({
|
||||
cronTime: '00 00 */4 * * *', // every 4 hours
|
||||
@@ -112,14 +109,28 @@ function recreateJobs(unusedTimeZone, callback) {
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
});
|
||||
|
||||
if (gCertificateRenewJob) gCertificateRenewJob.stop();
|
||||
gCertificateRenewJob = new CronJob({
|
||||
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||
onTick: certificates.autoRenew,
|
||||
start: true,
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
});
|
||||
|
||||
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
|
||||
|
||||
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
|
||||
function autoupdatePatternChanged(pattern) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
assert(gBoxUpdateCheckerJob);
|
||||
|
||||
debug('Auto update pattern changed to %s', pattern);
|
||||
|
||||
@@ -149,33 +160,37 @@ function autoupdatePatternChanged(pattern) {
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!gInitialized) return callback();
|
||||
cloudron.events.removeListener(cloudron.EVENT_ACTIVATED, recreateJobs);
|
||||
|
||||
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
|
||||
if (gAutoupdaterJob) gAutoupdaterJob.stop();
|
||||
gAutoupdaterJob = null;
|
||||
|
||||
gBoxUpdateCheckerJob.stop();
|
||||
if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop();
|
||||
gBoxUpdateCheckerJob = null;
|
||||
|
||||
gAppUpdateCheckerJob.stop();
|
||||
if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop();
|
||||
gAppUpdateCheckerJob = null;
|
||||
|
||||
gHeartbeatJob.stop();
|
||||
if (gHeartbeatJob) gHeartbeatJob.stop();
|
||||
gHeartbeatJob = null;
|
||||
|
||||
gBackupJob.stop();
|
||||
if (gBackupJob) gBackupJob.stop();
|
||||
gBackupJob = null;
|
||||
|
||||
gCleanupTokensJob.stop();
|
||||
if (gCleanupTokensJob) gCleanupTokensJob.stop();
|
||||
gCleanupTokensJob = null;
|
||||
|
||||
gDockerVolumeCleanerJob.stop();
|
||||
if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop();
|
||||
gDockerVolumeCleanerJob = null;
|
||||
|
||||
gSchedulerSyncJob.stop();
|
||||
if (gSchedulerSyncJob) gSchedulerSyncJob.stop();
|
||||
gSchedulerSyncJob = null;
|
||||
|
||||
gInitialized = false;
|
||||
if (gCertificateRenewJob) gCertificateRenewJob.stop();
|
||||
gCertificateRenewJob = null;
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -126,6 +126,8 @@ function clear(callback) {
|
||||
function beginTransaction(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gConnectionPool === null) return callback(new Error('No database connection pool.'));
|
||||
|
||||
gConnectionPool.getConnection(function (error, connection) {
|
||||
if (error) return callback(error);
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ function DeveloperError(reason, errorOrMessage) {
|
||||
}
|
||||
util.inherits(DeveloperError, Error);
|
||||
DeveloperError.INTERNAL_ERROR = 'Internal Error';
|
||||
DeveloperError.EXTERNAL_ERROR = 'External Error';
|
||||
|
||||
function enabled(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -77,8 +78,8 @@ function getNonApprovedApps(callback) {
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
|
||||
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).end(function (error, result) {
|
||||
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
|
||||
if (result.status !== 200) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
|
||||
if (error && !error.response) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 200) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null, result.body.apps || []);
|
||||
});
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
checkPtrRecord: checkPtrRecord
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:digitalocean'),
|
||||
dns = require('native-dns');
|
||||
|
||||
function checkPtrRecord(ip, fqdn, callback) {
|
||||
assert(ip === null || typeof ip === 'string');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('checkPtrRecord: ' + ip);
|
||||
|
||||
if (!ip) return callback(new Error('Network down'));
|
||||
|
||||
dns.resolve4('ns1.digitalocean.com', function (error, rdnsIps) {
|
||||
if (error || rdnsIps.length === 0) return callback(new Error('Failed to query DO DNS'));
|
||||
|
||||
var reversedIp = ip.split('.').reverse().join('.');
|
||||
|
||||
var req = dns.Request({
|
||||
question: dns.Question({ name: reversedIp + '.in-addr.arpa', type: 'PTR' }),
|
||||
server: { address: rdnsIps[0] },
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
req.on('timeout', function () { return callback(new Error('Timedout')); });
|
||||
|
||||
req.on('message', function (error, message) {
|
||||
if (error || !message.answer || message.answer.length === 0) return callback(new Error('Failed to query PTR'));
|
||||
|
||||
debug('checkPtrRecord: Actual:%s Expecting:%s', message.answer[0].data, fqdn);
|
||||
callback(null, message.answer[0].data === fqdn);
|
||||
});
|
||||
|
||||
req.send();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
del: del,
|
||||
update: update,
|
||||
getChangeStatus: getChangeStatus,
|
||||
get: get
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/caas'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
|
||||
|
||||
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
values: values
|
||||
};
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
|
||||
.query({ token: dnsConfig.token })
|
||||
.send(data)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
|
||||
if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null, result.body.changeId);
|
||||
});
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
|
||||
|
||||
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', zoneName, subdomain, type, fqdn);
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
|
||||
.query({ token: dnsConfig.token, type: type })
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null, result.body.values);
|
||||
});
|
||||
}
|
||||
|
||||
function update(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (_.isEqual(values, result)) return callback();
|
||||
|
||||
add(dnsConfig, zoneName, subdomain, type, values, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
values: values
|
||||
};
|
||||
|
||||
superagent
|
||||
.del(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain))
|
||||
.query({ token: dnsConfig.token })
|
||||
.send(data)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
|
||||
if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND));
|
||||
if (result.statusCode !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getChangeStatus(dnsConfig, changeId, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof changeId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (changeId === '') return callback(null, 'INSYNC');
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/domains/' + config.fqdn() + '/status/' + changeId)
|
||||
.query({ token: dnsConfig.token })
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null, result.body.status);
|
||||
});
|
||||
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
get: get,
|
||||
del: del,
|
||||
update: update,
|
||||
getChangeStatus: getChangeStatus
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
AWS = require('aws-sdk'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/route53'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
function getDnsCredentials(dnsConfig) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
|
||||
var credentials = {
|
||||
accessKeyId: dnsConfig.accessKeyId,
|
||||
secretAccessKey: dnsConfig.secretAccessKey,
|
||||
region: dnsConfig.region
|
||||
};
|
||||
|
||||
if (dnsConfig.endpoint) credentials.endpoint = new AWS.Endpoint(dnsConfig.endpoint);
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.listHostedZones({}, function (error, result) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
|
||||
var zone = result.HostedZones.filter(function (zone) {
|
||||
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
|
||||
})[0];
|
||||
|
||||
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
|
||||
|
||||
callback(null, zone);
|
||||
});
|
||||
}
|
||||
|
||||
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
|
||||
getZoneByName(dnsConfig, zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var fqdn = config.appFqdn(subdomain);
|
||||
var records = values.map(function (v) { return { Value: v }; });
|
||||
|
||||
var params = {
|
||||
ChangeBatch: {
|
||||
Changes: [{
|
||||
Action: 'UPSERT',
|
||||
ResourceRecordSet: {
|
||||
Type: type,
|
||||
Name: fqdn,
|
||||
ResourceRecords: records,
|
||||
TTL: 1
|
||||
}
|
||||
}]
|
||||
},
|
||||
HostedZoneId: zone.Id
|
||||
};
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.code === 'PriorRequestNotComplete') {
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
|
||||
} else if (error) {
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
callback(null, result.ChangeInfo.Id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function update(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (_.isEqual(values, result)) return callback();
|
||||
|
||||
add(dnsConfig, zoneName, subdomain, type, values, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getZoneByName(dnsConfig, zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var params = {
|
||||
HostedZoneId: zone.Id,
|
||||
MaxItems: '1',
|
||||
StartRecordName: (subdomain ? subdomain + '.' : '') + zoneName + '.',
|
||||
StartRecordType: type
|
||||
};
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.listResourceRecordSets(params, function (error, result) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
if (result.ResourceRecordSets.length === 0) return callback(null, [ ]);
|
||||
if (result.ResourceRecordSets[0].Name !== params.StartRecordName && result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]);
|
||||
|
||||
var values = result.ResourceRecordSets[0].ResourceRecords.map(function (record) { return record.Value; });
|
||||
|
||||
callback(null, values);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getZoneByName(dnsConfig, zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var fqdn = config.appFqdn(subdomain);
|
||||
var records = values.map(function (v) { return { Value: v }; });
|
||||
|
||||
var resourceRecordSet = {
|
||||
Name: fqdn,
|
||||
Type: type,
|
||||
ResourceRecords: records,
|
||||
TTL: 1
|
||||
};
|
||||
|
||||
var params = {
|
||||
ChangeBatch: {
|
||||
Changes: [{
|
||||
Action: 'DELETE',
|
||||
ResourceRecordSet: resourceRecordSet
|
||||
}]
|
||||
},
|
||||
HostedZoneId: zone.Id
|
||||
};
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
|
||||
debug('delSubdomain: resource record set not found.', error);
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error && error.code === 'NoSuchHostedZone') {
|
||||
debug('delSubdomain: hosted zone not found.', error);
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error && error.code === 'PriorRequestNotComplete') {
|
||||
debug('delSubdomain: resource is still busy', error);
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
|
||||
} else if (error && error.code === 'InvalidChangeBatch') {
|
||||
debug('delSubdomain: invalid change batch. No such record to be deleted.');
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error) {
|
||||
debug('delSubdomain: error', error);
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getChangeStatus(dnsConfig, changeId, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof changeId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (changeId === '') return callback(null, 'INSYNC');
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.getChange({ Id: changeId }, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result.ChangeInfo.Status);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ var addons = require('./addons.js'),
|
||||
Docker = require('dockerode'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
util = require('util');
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
exports = module.exports = {
|
||||
connection: connectionInstance(),
|
||||
@@ -64,18 +65,17 @@ function pullImage(manifest, callback) {
|
||||
// is emitted as a chunk
|
||||
stream.on('data', function (chunk) {
|
||||
var data = safe.JSON.parse(chunk) || { };
|
||||
debug('pullImage data: %j', data);
|
||||
debug('pullImage %s: %j', manifest.id, 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 }
|
||||
} else if (data.error) {
|
||||
debug('pullImage error detail: %s', data.errorDetail.message);
|
||||
debug('pullImage error %s: %s', manifest.id, data.errorDetail.message);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', function () {
|
||||
debug('downloaded image %s successfully', manifest.dockerImage);
|
||||
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
|
||||
|
||||
var image = docker.getImage(manifest.dockerImage);
|
||||
|
||||
@@ -84,14 +84,14 @@ function pullImage(manifest, callback) {
|
||||
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'));
|
||||
|
||||
debug('This image exposes ports: %j', data.Config.ExposedPorts);
|
||||
debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('error', function (error) {
|
||||
debug('error pulling image %s : %j', manifest.dockerImage, error);
|
||||
debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
@@ -102,12 +102,12 @@ function downloadImage(manifest, callback) {
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('downloadImage %s', manifest.dockerImage);
|
||||
debug('downloadImage %s %s', manifest.id, manifest.dockerImage);
|
||||
|
||||
var attempt = 1;
|
||||
|
||||
async.retry({ times: 5, interval: 15000 }, function (retryCallback) {
|
||||
debug('Downloading image. attempt: %s', attempt++);
|
||||
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
|
||||
debug('Downloading image %s %s. attempt: %s', manifest.id, manifest.dockerImage, attempt++);
|
||||
|
||||
pullImage(manifest, function (error) {
|
||||
if (error) console.error(error);
|
||||
@@ -117,9 +117,11 @@ function downloadImage(manifest, callback) {
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function createSubcontainer(app, cmd, callback) {
|
||||
function createSubcontainer(app, name, cmd, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert(!cmd || util.isArray(cmd));
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var docker = exports.connection,
|
||||
@@ -129,8 +131,10 @@ function createSubcontainer(app, cmd, callback) {
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
var stdEnv = [
|
||||
'CLOUDRON=1',
|
||||
'WEBADMIN_ORIGIN' + '=' + config.adminOrigin(),
|
||||
'API_ORIGIN' + '=' + config.adminOrigin()
|
||||
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
|
||||
'API_ORIGIN=' + config.adminOrigin(),
|
||||
'APP_ORIGIN=https://' + config.appFqdn(app.location),
|
||||
'APP_DOMAIN=' + config.appFqdn(app.location)
|
||||
];
|
||||
|
||||
// docker portBindings requires ports to be exposed
|
||||
@@ -150,12 +154,18 @@ function createSubcontainer(app, cmd, callback) {
|
||||
}
|
||||
|
||||
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
|
||||
// for subcontainers, this should ideally be false. but docker does not allow network sharing if the app container is not running
|
||||
// this means cloudron exec does not work
|
||||
var isolatedNetworkNs = true;
|
||||
|
||||
addons.getEnvironment(app, function (error, addonEnv) {
|
||||
if (error) return callback(new Error('Error getting addon environment : ' + error));
|
||||
|
||||
var containerOptions = {
|
||||
Hostname: config.appFqdn(app.location),
|
||||
name: name, // used for filtering logs
|
||||
// do _not_ set hostname to app fqdn. doing so sets up the dns name to look up the internal docker ip. this makes curl from within container fail
|
||||
// for subcontainers, this should not be set because we already share the network namespace with app container
|
||||
Hostname: isolatedNetworkNs ? (semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location)) : null,
|
||||
Tty: isAppContainer,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: cmd,
|
||||
@@ -177,28 +187,30 @@ function createSubcontainer(app, cmd, callback) {
|
||||
PortBindings: isAppContainer ? 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": isAppContainer ? "always" : "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
CpuShares: 512, // relative to 1024 for system processes
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ":rw" ],
|
||||
NetworkMode: isolatedNetworkNs ? 'default' : ('container:' + app.containerId), // share network namespace with parent
|
||||
Links: isolatedNetworkNs ? addons.getLinksSync(app, app.manifest.addons) : null, // links is redundant with --net=container
|
||||
SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron
|
||||
}
|
||||
};
|
||||
containerOptions = _.extend(containerOptions, options);
|
||||
|
||||
// older versions wanted a writable /var/log
|
||||
if (semver.lte(targetBoxVersion(app.manifest), '0.0.71')) containerOptions.Volumes['/var/log'] = {};
|
||||
|
||||
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
|
||||
debugApp(app, 'Creating container for %s with options %j', app.manifest.dockerImage, containerOptions);
|
||||
|
||||
docker.createContainer(containerOptions, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function createContainer(app, callback) {
|
||||
createSubcontainer(app, null, callback);
|
||||
createSubcontainer(app, app.id /* name */, null /* cmd */, { } /* options */, callback);
|
||||
}
|
||||
|
||||
function startContainer(containerId, callback) {
|
||||
|
||||
@@ -5,8 +5,7 @@ Dear Admin,
|
||||
The application titled '<%= title %>' that you installed at <%= appFqdn %>
|
||||
is not responding.
|
||||
|
||||
This is most likely a problem in the application. Please report this issue to
|
||||
support@cloudron.io (by forwarding this email).
|
||||
This is most likely a problem in the application.
|
||||
|
||||
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
Dear <%= user.username %>,
|
||||
|
||||
I am excited to welcome you to my Cloudron <%= fqdn %>!
|
||||
Welcome to my Cloudron <%= fqdn %>!
|
||||
|
||||
The Cloudron is our own Private Cloud. You can read more about it
|
||||
The Cloudron is our own Smart Server. You can read more about it
|
||||
at https://www.cloudron.io.
|
||||
|
||||
You username is '<%= user.username %>'
|
||||
|
||||
@@ -25,16 +25,16 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:mailer'),
|
||||
digitalocean = require('./digitalocean.js'),
|
||||
dns = require('native-dns'),
|
||||
docker = require('./docker.js').connection,
|
||||
ejs = require('ejs'),
|
||||
nodemailer = require('nodemailer'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
smtpTransport = require('nodemailer-smtp-transport'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
userdb = require('./userdb.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
@@ -48,13 +48,20 @@ var gMailQueue = [ ],
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
checkDns();
|
||||
if (cloudron.isConfiguredSync()) {
|
||||
checkDns();
|
||||
} else {
|
||||
cloudron.events.on(cloudron.EVENT_CONFIGURED, checkDns);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, checkDns);
|
||||
|
||||
// TODO: interrupt processQueue as well
|
||||
clearTimeout(gCheckDnsTimerId);
|
||||
gCheckDnsTimerId = null;
|
||||
@@ -65,20 +72,76 @@ function uninitialize(callback) {
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function getTxtRecords(callback) {
|
||||
dns.resolveNs(config.zoneName(), function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(error || new Error('Unable to get nameservers'));
|
||||
|
||||
var nameserver = nameservers[0];
|
||||
|
||||
dns.resolve4(nameserver, function (error, nsIps) {
|
||||
if (error || !nsIps || nsIps.length === 0) return callback(error);
|
||||
|
||||
var req = dns.Request({
|
||||
question: dns.Question({ name: config.fqdn(), type: 'TXT' }),
|
||||
server: { address: nsIps[0] },
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
req.on('timeout', function () { return callback(new Error('ETIMEOUT')); });
|
||||
|
||||
req.on('message', function (error, message) {
|
||||
if (error || !message.answer || message.answer.length === 0) return callback(null, null);
|
||||
|
||||
var records = message.answer.map(function (a) { return a.data[0]; });
|
||||
callback(null, records);
|
||||
});
|
||||
|
||||
req.send();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkDns() {
|
||||
digitalocean.checkPtrRecord(sysinfo.getIp(), config.fqdn(), function (error, ok) {
|
||||
if (error || !ok) {
|
||||
debug('PTR record not setup yet');
|
||||
gCheckDnsTimerId = setTimeout(checkDns, 10000);
|
||||
getTxtRecords(function (error, records) {
|
||||
if (error || !records) {
|
||||
debug('checkDns: DNS error or no records looking up TXT records for %s %s', config.fqdn(), error, records);
|
||||
gCheckDnsTimerId = setTimeout(checkDns, 60000);
|
||||
return;
|
||||
}
|
||||
|
||||
var allowedToSendMail = false;
|
||||
|
||||
for (var i = 0; i < records.length; i++) {
|
||||
if (records[i].indexOf('v=spf1 ') !== 0) continue; // not SPF
|
||||
|
||||
allowedToSendMail = records[i].indexOf('a:' + config.fqdn()) !== -1;
|
||||
break; // only one SPF record can exist (https://support.google.com/a/answer/4568483?hl=en)
|
||||
}
|
||||
|
||||
if (!allowedToSendMail) {
|
||||
debug('checkDns: SPF records disallow sending email from cloudron. %j', records);
|
||||
gCheckDnsTimerId = setTimeout(checkDns, 60000);
|
||||
return;
|
||||
}
|
||||
|
||||
debug('checkDns: SPF check passed. commencing mail processing');
|
||||
gDnsReady = true;
|
||||
processQueue();
|
||||
});
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
assert(gDnsReady);
|
||||
|
||||
sendMails(gMailQueue);
|
||||
gMailQueue = [ ];
|
||||
}
|
||||
|
||||
// note : this function should NOT access the database. it is called by the crashnotifier
|
||||
// which does not initialize mailer or the databse
|
||||
function sendMails(queue) {
|
||||
assert(util.isArray(queue));
|
||||
|
||||
docker.getContainer('mail').inspect(function (error, data) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
@@ -90,12 +153,9 @@ function processQueue() {
|
||||
port: 2500 // this value comes from mail container
|
||||
}));
|
||||
|
||||
var mailQueueCopy = gMailQueue;
|
||||
gMailQueue = [ ];
|
||||
debug('Processing mail queue of size %d (through %s:2500)', queue.length, mailServerIp);
|
||||
|
||||
debug('Processing mail queue of size %d (through %s:2500)', mailQueueCopy.length, mailServerIp);
|
||||
|
||||
async.mapSeries(mailQueueCopy, function iterator(mailOptions, callback) {
|
||||
async.mapSeries(queue, function iterator(mailOptions, callback) {
|
||||
transport.sendMail(mailOptions, function (error) {
|
||||
if (error) return console.error(error); // TODO: requeue?
|
||||
debug('Email sent to ' + mailOptions.to);
|
||||
@@ -224,7 +284,7 @@ function appDied(app) {
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: adminEmails.join(', '),
|
||||
to: adminEmails.concat('support@cloudron.io').join(', '),
|
||||
subject: util.format('App %s is down', app.location),
|
||||
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: config.appFqdn(app.location), format: 'text' })
|
||||
};
|
||||
@@ -269,6 +329,8 @@ function appUpdateAvailable(app, updateInfo) {
|
||||
});
|
||||
}
|
||||
|
||||
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
|
||||
// crashnotifier should be able to send mail when there is no db
|
||||
function sendCrashNotification(program, context) {
|
||||
assert.strictEqual(typeof program, 'string');
|
||||
assert.strictEqual(typeof context, 'string');
|
||||
@@ -280,7 +342,7 @@ function sendCrashNotification(program, context) {
|
||||
text: render('crash_notification.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
sendMails([ mailOptions ]);
|
||||
}
|
||||
|
||||
function sendFeedback(user, type, subject, description) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:src/nginx'),
|
||||
ejs = require('ejs'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js');
|
||||
|
||||
exports = module.exports = {
|
||||
configureAdmin: configureAdmin,
|
||||
configureApp: configureApp,
|
||||
unconfigureApp: unconfigureApp,
|
||||
reload: reload
|
||||
};
|
||||
|
||||
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
|
||||
|
||||
function configureAdmin(certFilePath, keyFilePath, callback) {
|
||||
assert.strictEqual(typeof certFilePath, 'string');
|
||||
assert.strictEqual(typeof keyFilePath, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = {
|
||||
sourceDir: path.resolve(__dirname, '..'),
|
||||
adminOrigin: config.adminOrigin(),
|
||||
vhost: config.adminFqdn(),
|
||||
endpoint: 'admin',
|
||||
certFilePath: certFilePath,
|
||||
keyFilePath: keyFilePath
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf');
|
||||
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
|
||||
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
function configureApp(app, certFilePath, keyFilePath, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof certFilePath, 'string');
|
||||
assert.strictEqual(typeof keyFilePath, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var sourceDir = path.resolve(__dirname, '..');
|
||||
var endpoint = app.oauthProxy ? 'oauthproxy' : 'app';
|
||||
var vhost = config.appFqdn(app.location);
|
||||
|
||||
var data = {
|
||||
sourceDir: sourceDir,
|
||||
adminOrigin: config.adminOrigin(),
|
||||
vhost: vhost,
|
||||
port: app.httpPort,
|
||||
endpoint: endpoint,
|
||||
certFilePath: certFilePath,
|
||||
keyFilePath: keyFilePath
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
debug('writing config for "%s" to %s', app.location, nginxConfigFilename);
|
||||
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
|
||||
debug('Error creating nginx config for "%s" : %s', app.location, safe.error.message);
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
function unconfigureApp(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
|
||||
debug('Error removing nginx configuration of "%s": %s', app.location, safe.error.message);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
function reload(callback) {
|
||||
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
|
||||
}
|
||||
@@ -35,9 +35,9 @@ args.forEach(function (arg) {
|
||||
});
|
||||
|
||||
if (code && redirectURI) {
|
||||
window.location.href = redirectURI + '?code=' + code + (state ? '&state=' + state : '');
|
||||
window.location.href = redirectURI + (redirectURI.indexOf('?') !== -1 ? '&' : '?') + 'code=' + code + (state ? '&state=' + state : '');
|
||||
} else if (redirectURI && accessToken) {
|
||||
window.location.href = redirectURI + '?token=' + accessToken + (state ? '&state=' + state : '');
|
||||
window.location.href = redirectURI + (redirectURI.indexOf('?') !== -1 ? '&' : '?') + 'token=' + accessToken + (state ? '&state=' + state : '');
|
||||
} else {
|
||||
window.location.href = '/api/v1/session/login';
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
<title> Cloudron Login </title>
|
||||
|
||||
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
|
||||
@@ -32,6 +34,7 @@
|
||||
<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">Cloudron</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -9,12 +9,14 @@ var appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:proxy'),
|
||||
express = require('express'),
|
||||
http = require('http'),
|
||||
proxy = require('proxy-middleware'),
|
||||
session = require('cookie-session'),
|
||||
superagent = require('superagent'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
url = require('url'),
|
||||
uuid = require('node-uuid');
|
||||
|
||||
@@ -24,13 +26,20 @@ var gHttpServer = null;
|
||||
|
||||
var CALLBACK_URI = '/callback';
|
||||
|
||||
function clearSession(req) {
|
||||
delete gSessions[req.session.id];
|
||||
|
||||
req.session.id = uuid.v4();
|
||||
gSessions[req.session.id] = {};
|
||||
|
||||
req.sessionData = gSessions[req.session.id];
|
||||
|
||||
}
|
||||
|
||||
function attachSessionData(req, res, next) {
|
||||
assert.strictEqual(typeof req.session, 'object');
|
||||
|
||||
if (!req.session.id || !gSessions[req.session.id]) {
|
||||
req.session.id = uuid.v4();
|
||||
gSessions[req.session.id] = {};
|
||||
}
|
||||
if (!req.session.id || !gSessions[req.session.id]) clearSession(req);
|
||||
|
||||
// attach the session data to the requeset
|
||||
req.sessionData = gSessions[req.session.id];
|
||||
@@ -46,16 +55,10 @@ function verifySession(req, res, next) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 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;
|
||||
} else if (result.statusCode !== 200) {
|
||||
req.sessionData.accessToken = null;
|
||||
tokendb.get(req.sessionData.accessToken, function (error, token) {
|
||||
if (error) {
|
||||
if (error.reason !== DatabaseError.NOT_FOUND) console.error(error);
|
||||
clearSession(req);
|
||||
req.authenticated = false;
|
||||
} else {
|
||||
req.authenticated = true;
|
||||
@@ -89,7 +92,7 @@ function authenticate(req, res, next) {
|
||||
.post(config.internalAdminOrigin() + '/api/v1/oauth/token')
|
||||
.query(query).send(data)
|
||||
.end(function (error, result) {
|
||||
if (error) {
|
||||
if (error && !error.response) {
|
||||
console.error(error);
|
||||
return res.send(500, 'Unable to contact the oauth server.');
|
||||
}
|
||||
|
||||
@@ -14,18 +14,22 @@ exports = module.exports = {
|
||||
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'data/addons'),
|
||||
SCHEDULER_FILE: path.join(config.baseDir(), 'data/addons/scheduler.json'),
|
||||
|
||||
DNS_IN_SYNC_FILE: path.join(config.baseDir(), 'data/dns_in_sync'),
|
||||
|
||||
COLLECTD_APPCONFIG_DIR: path.join(config.baseDir(), 'data/collectd/collectd.conf.d'),
|
||||
|
||||
DATA_DIR: path.join(config.baseDir(), 'data'),
|
||||
BOX_DATA_DIR: path.join(config.baseDir(), 'data/box'),
|
||||
// this is not part of appdata because an icon may be set before install
|
||||
APPICONS_DIR: path.join(config.baseDir(), 'data/box/appicons'),
|
||||
APP_CERTS_DIR: path.join(config.baseDir(), 'data/box/certs'),
|
||||
MAIL_DATA_DIR: path.join(config.baseDir(), 'data/box/mail'),
|
||||
|
||||
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'),
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json'),
|
||||
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json')
|
||||
ACME_CHALLENGES_DIR: path.join(config.baseDir(), 'data/acme'),
|
||||
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'data/box/acme/acme.key')
|
||||
};
|
||||
|
||||
@@ -117,18 +117,24 @@ function installApp(req, res, next) {
|
||||
if (typeof data.accessRestriction !== 'object') 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'));
|
||||
if (data.cert && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
if (data.key && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
|
||||
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
|
||||
|
||||
// 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 accessRestriction:%j 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.oauthProxy, data.icon || null, function (error) {
|
||||
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, data.icon || null, data.cert || null, data.key || 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.'));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.USER_REQUIRED) return next(new HttpError(400, 'accessRestriction must specify one user'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -154,16 +160,21 @@ function configureApp(req, res, next) {
|
||||
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
|
||||
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
|
||||
if (data.cert && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
if (data.key && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
|
||||
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
|
||||
|
||||
debug('Configuring app id:%s location:%s bindings:%j accessRestriction:%j oauthProxy:%s', req.params.id, data.location, data.portBindings, data.accessRestriction, data.oauthProxy);
|
||||
|
||||
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, function (error) {
|
||||
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, data.cert || null, data.key || 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.'));
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
@@ -276,14 +287,14 @@ function getLogStream(req, res, next) {
|
||||
|
||||
debug('Getting logstream of app id:%s', req.params.id);
|
||||
|
||||
var fromLine = req.query.fromLine ? parseInt(req.query.fromLine, 10) : -10; // we ignore last-event-id
|
||||
if (isNaN(fromLine)) return next(new HttpError(400, 'fromLine must be a valid number'));
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
|
||||
|
||||
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
|
||||
|
||||
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
|
||||
|
||||
apps.getLogStream(req.params.id, fromLine, function (error, logStream) {
|
||||
apps.getLogs(req.params.id, lines, true /* follow */, function (error, logStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -299,7 +310,7 @@ function getLogStream(req, res, next) {
|
||||
res.on('close', logStream.close);
|
||||
logStream.on('data', function (data) {
|
||||
var obj = JSON.parse(data);
|
||||
res.write(sse(obj.lineNumber, JSON.stringify(obj)));
|
||||
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
|
||||
});
|
||||
logStream.on('end', res.end.bind(res));
|
||||
logStream.on('error', res.end.bind(res, null));
|
||||
@@ -309,9 +320,12 @@ function getLogStream(req, res, next) {
|
||||
function getLogs(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
|
||||
|
||||
debug('Getting logs of app id:%s', req.params.id);
|
||||
|
||||
apps.getLogs(req.params.id, function (error, logStream) {
|
||||
apps.getLogs(req.params.id, lines, false /* follow */, function (error, logStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -355,6 +369,8 @@ function exec(req, res, next) {
|
||||
|
||||
duplexStream.pipe(res.socket);
|
||||
res.socket.pipe(duplexStream);
|
||||
|
||||
res.on('close', duplexStream.close);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ exports = module.exports = {
|
||||
getConfig: getConfig,
|
||||
update: update,
|
||||
migrate: migrate,
|
||||
setCertificate: setCertificate,
|
||||
feedback: feedback
|
||||
};
|
||||
|
||||
@@ -25,7 +24,6 @@ var assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
superagent = require('superagent'),
|
||||
safe = require('safetydance'),
|
||||
updateChecker = require('../updatechecker.js');
|
||||
|
||||
/**
|
||||
@@ -61,7 +59,7 @@ function activate(req, res, next) {
|
||||
|
||||
// Now let the api server know we got activated
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken:req.query.setupToken }).end(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 201) return next(new HttpError(500, result.text ? result.text.message : 'Internal error'));
|
||||
@@ -77,7 +75,7 @@ function setupTokenAuth(req, res, next) {
|
||||
if (typeof req.query.setupToken !== 'string') return next(new HttpError(400, 'no setupToken provided'));
|
||||
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken }).end(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 200) return next(new HttpError(500, result.text ? result.text.message : 'Internal error'));
|
||||
@@ -140,23 +138,6 @@ function migrate(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function setCertificate(req, res, next) {
|
||||
assert.strictEqual(typeof req.files, 'object');
|
||||
|
||||
if (!req.files.certificate) return next(new HttpError(400, 'certificate must be provided'));
|
||||
var certificate = safe.fs.readFileSync(req.files.certificate.path, 'utf8');
|
||||
|
||||
if (!req.files.key) return next(new HttpError(400, 'key must be provided'));
|
||||
var key = safe.fs.readFileSync(req.files.key.path, 'utf8');
|
||||
|
||||
cloudron.setCertificate(certificate, key, function (error) {
|
||||
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function feedback(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ var authorization = [
|
||||
var redirectPath = url.parse(redirectURI).path;
|
||||
var redirectOrigin = client.redirectURI;
|
||||
|
||||
callback(null, client, '/api/v1/session/callback?redirectURI=' + url.resolve(redirectOrigin, redirectPath));
|
||||
callback(null, client, '/api/v1/session/callback?redirectURI=' + encodeURIComponent(url.resolve(redirectOrigin, redirectPath)));
|
||||
});
|
||||
}),
|
||||
function (req, res, next) {
|
||||
|
||||
@@ -10,10 +10,21 @@ exports = module.exports = {
|
||||
setCloudronName: setCloudronName,
|
||||
|
||||
getCloudronAvatar: getCloudronAvatar,
|
||||
setCloudronAvatar: setCloudronAvatar
|
||||
setCloudronAvatar: setCloudronAvatar,
|
||||
|
||||
getDnsConfig: getDnsConfig,
|
||||
setDnsConfig: setDnsConfig,
|
||||
|
||||
getBackupConfig: getBackupConfig,
|
||||
setBackupConfig: setBackupConfig,
|
||||
|
||||
setCertificate: setCertificate,
|
||||
setAdminCertificate: setAdminCertificate
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
certificates = require('../certificates.js'),
|
||||
CertificatesError = require('../certificates.js').CertificatesError,
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
safe = require('safetydance'),
|
||||
@@ -83,3 +94,75 @@ function getCloudronAvatar(req, res, next) {
|
||||
res.status(200).send(avatar);
|
||||
});
|
||||
}
|
||||
|
||||
function getDnsConfig(req, res, next) {
|
||||
settings.getDnsConfig(function (error, config) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, config));
|
||||
});
|
||||
}
|
||||
|
||||
function setDnsConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||
|
||||
settings.setDnsConfig(req.body, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
});
|
||||
}
|
||||
|
||||
function getBackupConfig(req, res, next) {
|
||||
settings.getBackupConfig(function (error, config) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, config));
|
||||
});
|
||||
}
|
||||
|
||||
function setBackupConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||
|
||||
settings.setBackupConfig(req.body, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
});
|
||||
}
|
||||
|
||||
// default fallback cert
|
||||
function setCertificate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (!req.body.cert || typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
|
||||
certificates.setFallbackCertificate(req.body.cert, req.body.key, function (error) {
|
||||
if (error && error.reason === CertificatesError.INVALID_CERT) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
// only webadmin cert, until it can be treated just like a normal app
|
||||
function setAdminCertificate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (!req.body.cert || typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
|
||||
certificates.setAdminCertificate(req.body.cert, req.body.key, function (error) {
|
||||
if (error && error.reason === CertificatesError.INVALID_CERT) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ var appdb = require('../../appdb.js'),
|
||||
nock = require('nock'),
|
||||
paths = require('../../paths.js'),
|
||||
redis = require('redis'),
|
||||
request = require('superagent'),
|
||||
superagent = require('superagent'),
|
||||
safe = require('safetydance'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js'),
|
||||
@@ -41,7 +41,7 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
// Test image information
|
||||
var TEST_IMAGE_REPO = 'cloudron/test';
|
||||
var TEST_IMAGE_TAG = '9.0.0';
|
||||
var TEST_IMAGE_TAG = '10.0.0';
|
||||
var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG).toString('utf8').trim();
|
||||
|
||||
var APP_STORE_ID = 'test', APP_ID;
|
||||
@@ -61,7 +61,7 @@ var USERNAME_1 = 'user', PASSWORD_1 = 'password', EMAIL_1 ='user@me.com';
|
||||
var token = null; // authentication token
|
||||
var token_1 = null;
|
||||
|
||||
var awsHostedZones = {
|
||||
var awsHostedZones = {
|
||||
HostedZones: [{
|
||||
Id: '/hostedzone/ZONEID',
|
||||
Name: 'localhost.',
|
||||
@@ -114,11 +114,10 @@ function setup(done) {
|
||||
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')
|
||||
superagent.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();
|
||||
@@ -137,21 +136,26 @@ function setup(done) {
|
||||
},
|
||||
|
||||
function (callback) {
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1 })
|
||||
.end(function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}, function (callback) {
|
||||
},
|
||||
|
||||
function (callback) {
|
||||
token_1 = tokendb.generateToken();
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 100000, '*', callback);
|
||||
}
|
||||
},
|
||||
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
|
||||
settings.setTlsConfig.bind(null, { provider: 'caas' }),
|
||||
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
|
||||
], done);
|
||||
}
|
||||
|
||||
@@ -192,174 +196,174 @@ describe('App API', function () {
|
||||
});
|
||||
|
||||
it('app install fails - missing manifest', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('manifest is required');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - missing appId', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: APP_MANIFEST, password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('appStoreId is required');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - invalid json', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send('garbage')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - invalid location', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: null, 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');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - invalid location type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: null, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('location is required');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - reserved admin location', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: null, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql(constants.ADMIN_LOCATION + ' is reserved');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - reserved api location', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: null, oauthProxy: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - portBindings must be object', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: null, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('portBindings must be an object');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - accessRestriction is required', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: {}, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('accessRestriction is required');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - accessRestriction type is wrong', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: '', oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('accessRestriction is required');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - accessRestriction no users not allowed', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST_1, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: null, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('accessRestriction must specify one user');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - accessRestriction too many users not allowed', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST_1, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: { users: [ 'one', 'two' ] }, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('accessRestriction must specify one user');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - oauthProxy is required', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('oauthProxy must be a boolean');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails for non admin', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: null, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails due to purchase failure', function (done) {
|
||||
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(402, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: null, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(402);
|
||||
expect(fake.isDone()).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install succeeds with purchase', function (done) {
|
||||
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: null, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
@@ -367,14 +371,14 @@ describe('App API', function () {
|
||||
expect(res.body.id).to.be.a('string');
|
||||
APP_ID = res.body.id;
|
||||
expect(fake.isDone()).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails because of conflicting location', function (done) {
|
||||
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: null, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
@@ -385,120 +389,120 @@ describe('App API', function () {
|
||||
});
|
||||
|
||||
it('can get app status', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
|
||||
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.id).to.eql(APP_ID);
|
||||
expect(res.body.installationState).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get invalid app status', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/apps/kubachi')
|
||||
superagent.get(SERVER_URL + '/api/v1/apps/kubachi')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get all apps', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/apps')
|
||||
superagent.get(SERVER_URL + '/api/v1/apps')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.apps).to.be.an('array');
|
||||
expect(res.body.apps[0].id).to.eql(APP_ID);
|
||||
expect(res.body.apps[0].installationState).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('non admin can get all apps', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/apps')
|
||||
superagent.get(SERVER_URL + '/api/v1/apps')
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.apps).to.be.an('array');
|
||||
expect(res.body.apps[0].id).to.eql(APP_ID);
|
||||
expect(res.body.apps[0].installationState).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get appBySubdomain', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/subdomains/' + APP_LOCATION)
|
||||
superagent.get(SERVER_URL + '/api/v1/subdomains/' + APP_LOCATION)
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.id).to.eql(APP_ID);
|
||||
expect(res.body.installationState).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get invalid app by Subdomain', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/subdomains/tikaloma')
|
||||
superagent.get(SERVER_URL + '/api/v1/subdomains/tikaloma')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot uninstall invalid app', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/whatever/uninstall')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/whatever/uninstall')
|
||||
.send({ password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot uninstall app without password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot uninstall app with wrong password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
.send({ password: PASSWORD+PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('non admin cannot uninstall app', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
.send({ password: PASSWORD })
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can uninstall app', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
.send({ password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('app install succeeds already purchased', function (done) {
|
||||
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: null, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
@@ -506,7 +510,7 @@ describe('App API', function () {
|
||||
expect(res.body.id).to.be.a('string');
|
||||
APP_ID = res.body.id;
|
||||
expect(fake.isDone()).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -516,7 +520,7 @@ describe('App API', function () {
|
||||
settings.setDeveloperMode(true, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: USERNAME, password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
@@ -527,7 +531,7 @@ describe('App API', function () {
|
||||
// overwrite non dev token
|
||||
token = result.body.token;
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: null, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
@@ -535,18 +539,18 @@ describe('App API', function () {
|
||||
expect(res.body.id).to.be.a('string');
|
||||
expect(fake.isDone()).to.be.ok();
|
||||
APP_ID = res.body.id;
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can uninstall app without password but developer token', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -584,10 +588,7 @@ describe('App installation', function () {
|
||||
function (callback) {
|
||||
apiHockInstance
|
||||
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
|
||||
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'))
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
|
||||
.max(Infinity)
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }, { 'Content-Type': 'application/json' });
|
||||
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'));
|
||||
|
||||
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
|
||||
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
|
||||
@@ -603,8 +604,7 @@ describe('App installation', function () {
|
||||
.max(Infinity)
|
||||
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'dnsrecordid', Status: 'INSYNC' } }), { 'Content-Type': 'application/xml' });
|
||||
|
||||
var port = parseInt(url.parse(config.aws().endpoint).port, 10);
|
||||
awsHockServer = http.createServer(awsHockInstance.handler).listen(port, callback);
|
||||
awsHockServer = http.createServer(awsHockInstance.handler).listen(5353, callback);
|
||||
}
|
||||
], done);
|
||||
});
|
||||
@@ -627,7 +627,7 @@ describe('App installation', function () {
|
||||
|
||||
var count = 0;
|
||||
function checkInstallStatus() {
|
||||
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
|
||||
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -638,7 +638,7 @@ describe('App installation', function () {
|
||||
});
|
||||
}
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: null, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
@@ -672,6 +672,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');
|
||||
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + config.appFqdn(APP_LOCATION));
|
||||
expect(data.Config.Env).to.contain('APP_DOMAIN=' + config.appFqdn(APP_LOCATION));
|
||||
expect(data.Config.Hostname).to.be(APP_LOCATION);
|
||||
clientdb.getByAppIdAndType(appResult.id, clientdb.TYPE_OAUTH, function (error, client) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens
|
||||
@@ -701,7 +704,7 @@ describe('App installation', function () {
|
||||
it('installation - is up and running', function (done) {
|
||||
expect(appResult.httpPort).to.be(undefined);
|
||||
setTimeout(function () {
|
||||
request.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
|
||||
superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
|
||||
.end(function (err, res) {
|
||||
expect(!err).to.be.ok();
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -837,8 +840,8 @@ describe('App installation', function () {
|
||||
}, done);
|
||||
});
|
||||
|
||||
it('logs - stdout and stderr', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logs')
|
||||
xit('logs - stdout and stderr', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logs')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
var data = '';
|
||||
@@ -851,8 +854,8 @@ describe('App installation', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('logStream - requires event-stream accept header', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logstream')
|
||||
xit('logStream - requires event-stream accept header', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logstream')
|
||||
.query({ access_token: token, fromLine: 0 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.be(400);
|
||||
@@ -861,7 +864,7 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
|
||||
it('logStream - stream logs', function (done) {
|
||||
xit('logStream - stream logs', function (done) {
|
||||
var options = {
|
||||
port: config.get('port'), host: 'localhost', path: '/api/v1/apps/' + APP_ID + '/logstream?access_token=' + token,
|
||||
headers: { 'Accept': 'text/event-stream', 'Connection': 'keep-alive' }
|
||||
@@ -891,7 +894,7 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
it('non admin cannot stop app', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
@@ -900,7 +903,7 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
it('can stop app', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
@@ -911,7 +914,7 @@ describe('App installation', function () {
|
||||
it('did stop the app', function (done) {
|
||||
// give the app a couple of seconds to die
|
||||
setTimeout(function () {
|
||||
request.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
|
||||
superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
|
||||
.end(function (err, res) {
|
||||
expect(err).to.be.ok();
|
||||
done();
|
||||
@@ -920,7 +923,7 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
it('nonadmin cannot start app', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/start')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/start')
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
@@ -929,7 +932,7 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
it('can start app', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/start')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/start')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
@@ -939,7 +942,7 @@ describe('App installation', function () {
|
||||
|
||||
it('did start the app', function (done) {
|
||||
setTimeout(function () {
|
||||
request.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
|
||||
superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
|
||||
.end(function (err, res) {
|
||||
expect(!err).to.be.ok();
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -951,7 +954,7 @@ describe('App installation', function () {
|
||||
it('can uninstall app', function (done) {
|
||||
var count = 0;
|
||||
function checkUninstallStatus() {
|
||||
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
|
||||
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
if (res.statusCode === 404) return done(null);
|
||||
@@ -960,7 +963,7 @@ describe('App installation', function () {
|
||||
});
|
||||
}
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
.send({ password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
@@ -1020,8 +1023,15 @@ describe('App installation - port bindings', function () {
|
||||
var awsHockInstance = hock.createHock({ throwOnUnmatched: false }), awsHockServer;
|
||||
var imageDeleted = false, imageCreated = false;
|
||||
|
||||
// *.foobar.com
|
||||
var validCert1 = '-----BEGIN CERTIFICATE-----\nMIIBvjCCAWgCCQCg957GWuHtbzANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEVMBMGA1UEAwwMKi5mb29iYXIuY29tMB4XDTE1\nMTAyODEzMDI1MFoXDTE2MTAyNzEzMDI1MFowZjELMAkGA1UEBhMCREUxDzANBgNV\nBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRAwDgYDVQQKDAdOZWJ1bG9uMQww\nCgYDVQQLDANDVE8xFTATBgNVBAMMDCouZm9vYmFyLmNvbTBcMA0GCSqGSIb3DQEB\nAQUAA0sAMEgCQQC0FKf07ZWMcABFlZw+GzXK9EiZrlJ1lpnu64RhN99z7MXRr8cF\nnZVgY3jgatuyR5s3WdzUvye2eJ0rNicl2EZJAgMBAAEwDQYJKoZIhvcNAQELBQAD\nQQAw4bteMZAeJWl2wgNLw+wTwAH96E0jyxwreCnT5AxJLmgimyQ0XOF4FsssdRFj\nxD9WA+rktelBodJyPeTDNhIh\n-----END CERTIFICATE-----';
|
||||
var validKey1 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBALQUp/TtlYxwAEWVnD4bNcr0SJmuUnWWme7rhGE333PsxdGvxwWd\nlWBjeOBq27JHmzdZ3NS/J7Z4nSs2JyXYRkkCAwEAAQJALV2eykcoC48TonQEPmkg\nbhaIS57syw67jMLsQImQ02UABKzqHPEKLXPOZhZPS9hsC/hGIehwiYCXMUlrl+WF\nAQIhAOntBI6qaecNjAAVG7UbZclMuHROUONmZUF1KNq6VyV5AiEAxRLkfHWy52CM\njOQrX347edZ30f4QczvugXwsyuU9A1ECIGlGZ8Sk4OBA8n6fAUcyO06qnmCJVlHg\npTUeOvKk5c9RAiBs28+8dCNbrbhVhx/yQr9FwNM0+ttJW/yWJ+pyNQhr0QIgJTT6\nxwCWYOtbioyt7B9l+ENy3AMSO3Uq+xmIKkvItK4=\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
before(function (done) {
|
||||
config.set('fqdn', 'test.foobar.com');
|
||||
|
||||
APP_ID = uuid.v4();
|
||||
|
||||
async.series([
|
||||
function (callback) {
|
||||
dockerProxy = startDockerProxy(function interceptor(req, res) {
|
||||
@@ -1045,15 +1055,16 @@ describe('App installation - port bindings', function () {
|
||||
function (callback) {
|
||||
apiHockInstance
|
||||
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
|
||||
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'))
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
|
||||
.max(Infinity)
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }, { 'Content-Type': 'application/json' });
|
||||
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'));
|
||||
|
||||
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
|
||||
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
|
||||
},
|
||||
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
|
||||
|
||||
settings.setTlsConfig.bind(null, { provider: 'caas' }),
|
||||
|
||||
function (callback) {
|
||||
awsHockInstance
|
||||
.get('/2013-04-01/hostedzone')
|
||||
@@ -1064,8 +1075,7 @@ describe('App installation - port bindings', function () {
|
||||
.max(Infinity)
|
||||
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'dnsrecordid', Status: 'INSYNC' } }), { 'Content-Type': 'application/xml' });
|
||||
|
||||
var port = parseInt(url.parse(config.aws().endpoint).port, 10);
|
||||
awsHockServer = http.createServer(awsHockInstance.handler).listen(port, callback);
|
||||
awsHockServer = http.createServer(awsHockInstance.handler).listen(5353, callback);
|
||||
}
|
||||
], done);
|
||||
});
|
||||
@@ -1087,7 +1097,7 @@ describe('App installation - port bindings', function () {
|
||||
|
||||
var count = 0;
|
||||
function checkInstallStatus() {
|
||||
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
|
||||
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -1098,7 +1108,7 @@ describe('App installation - port bindings', function () {
|
||||
});
|
||||
}
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/install')
|
||||
superagent.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: null, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
@@ -1154,7 +1164,7 @@ describe('App installation - port bindings', function () {
|
||||
var tryCount = 20;
|
||||
expect(appResult.httpPort).to.be(undefined);
|
||||
(function healthCheck() {
|
||||
request.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
|
||||
superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
|
||||
.end(function (err, res) {
|
||||
if (err || res.statusCode !== 200) {
|
||||
if (--tryCount === 0) return done(new Error('Timedout'));
|
||||
@@ -1244,7 +1254,7 @@ describe('App installation - port bindings', function () {
|
||||
assert.strictEqual(typeof count, 'number');
|
||||
assert.strictEqual(typeof done, 'function');
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
|
||||
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -1256,7 +1266,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')
|
||||
superagent.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: null, oauthProxy: true })
|
||||
.end(function (err, res) {
|
||||
@@ -1266,7 +1276,7 @@ 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')
|
||||
superagent.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 }, oauthProxy: false })
|
||||
.end(function (err, res) {
|
||||
@@ -1276,7 +1286,7 @@ describe('App installation - port bindings', function () {
|
||||
});
|
||||
|
||||
it('cannot reconfigure app with missing oauthProxy', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
superagent.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: null })
|
||||
.end(function (err, res) {
|
||||
@@ -1285,8 +1295,48 @@ describe('App installation - port bindings', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot reconfigure app with only the cert, no key', function (done) {
|
||||
superagent.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: null, oauthProxy: true, cert: validCert1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot reconfigure app with only the key, no cert', function (done) {
|
||||
superagent.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: null, oauthProxy: true, key: validKey1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot reconfigure app with cert not bein a string', function (done) {
|
||||
superagent.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: null, oauthProxy: true, cert: 1234, key: validKey1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot reconfigure app with key not bein a string', function (done) {
|
||||
superagent.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: null, oauthProxy: true, cert: validCert1, key: 1234 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('non admin cannot reconfigure app', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
superagent.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: null, oauthProxy: true })
|
||||
.end(function (err, res) {
|
||||
@@ -1296,7 +1346,7 @@ describe('App installation - port bindings', function () {
|
||||
});
|
||||
|
||||
it('can reconfigure app', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
superagent.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: null, oauthProxy: true })
|
||||
.end(function (err, res) {
|
||||
@@ -1379,8 +1429,18 @@ describe('App installation - port bindings', function () {
|
||||
}, done);
|
||||
});
|
||||
|
||||
it('can reconfigure app with custom certificate', function (done) {
|
||||
superagent.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: null, oauthProxy: true, cert: validCert1, key: validKey1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
checkConfigureStatus(0, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('can stop app', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
@@ -1405,7 +1465,7 @@ describe('App installation - port bindings', function () {
|
||||
it('can uninstall app', function (done) {
|
||||
var count = 0;
|
||||
function checkUninstallStatus() {
|
||||
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
|
||||
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
if (res.statusCode === 404) return done(null);
|
||||
@@ -1414,7 +1474,7 @@ describe('App installation - port bindings', function () {
|
||||
});
|
||||
}
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
.send({ password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
|
||||
@@ -11,8 +11,9 @@ var appdb = require('../../appdb.js'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
request = require('superagent'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js'),
|
||||
nock = require('nock'),
|
||||
userdb = require('../../userdb.js');
|
||||
|
||||
@@ -32,11 +33,10 @@ function setup(done) {
|
||||
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')
|
||||
superagent.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();
|
||||
@@ -52,6 +52,10 @@ 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 */, null /* accessRestriction */, false /* oauthProxy */, callback);
|
||||
},
|
||||
|
||||
function createSettings(callback) {
|
||||
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
@@ -69,22 +73,22 @@ describe('Backups API', function () {
|
||||
after(cleanup);
|
||||
|
||||
describe('get', function () {
|
||||
it('cannot get backups with appstore request failing', function (done) {
|
||||
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=APPSTORE_TOKEN').reply(401, {});
|
||||
it('cannot get backups with appstore superagent failing', function (done) {
|
||||
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=BACKUP_TOKEN').reply(401, {});
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/backups')
|
||||
superagent.get(SERVER_URL + '/api/v1/backups')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(503);
|
||||
expect(req.isDone()).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get backups', function (done) {
|
||||
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=APPSTORE_TOKEN').reply(200, { backups: ['foo', 'bar']});
|
||||
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=BACKUP_TOKEN').reply(200, { backups: ['foo', 'bar']});
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/backups')
|
||||
superagent.get(SERVER_URL + '/api/v1/backups')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -92,26 +96,24 @@ describe('Backups API', function () {
|
||||
expect(res.body.backups).to.be.an(Array);
|
||||
expect(res.body.backups[0]).to.eql('foo');
|
||||
expect(res.body.backups[1]).to.eql('bar');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', function () {
|
||||
it('fails due to mising token', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/backups')
|
||||
superagent.post(SERVER_URL + '/api/v1/backups')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong token', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/backups')
|
||||
superagent.post(SERVER_URL + '/api/v1/backups')
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -119,13 +121,12 @@ describe('Backups API', function () {
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var scope = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/backups')
|
||||
superagent.post(SERVER_URL + '/api/v1/backups')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
@@ -141,4 +142,3 @@ describe('Backups API', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ describe('OAuth Clients API', function () {
|
||||
.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.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
@@ -73,7 +72,6 @@ describe('OAuth Clients API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(412);
|
||||
done();
|
||||
});
|
||||
@@ -89,7 +87,6 @@ describe('OAuth Clients API', function () {
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -100,7 +97,6 @@ describe('OAuth Clients API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ redirectURI: 'http://foobar.com', scope: 'profile' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -111,7 +107,6 @@ describe('OAuth Clients API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ appId: '', redirectURI: 'http://foobar.com', scope: 'profile' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -122,7 +117,6 @@ describe('OAuth Clients API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -133,7 +127,6 @@ describe('OAuth Clients API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -144,7 +137,6 @@ describe('OAuth Clients API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', scope: 'profile' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -155,7 +147,6 @@ describe('OAuth Clients API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', redirectURI: '', scope: 'profile' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -166,7 +157,6 @@ describe('OAuth Clients API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', redirectURI: 'foobar', scope: 'profile' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -177,7 +167,6 @@ describe('OAuth Clients API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(result.body.id).to.be.a('string');
|
||||
expect(result.body.appId).to.be.a('string');
|
||||
@@ -211,7 +200,6 @@ describe('OAuth Clients API', function () {
|
||||
.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(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
@@ -230,7 +218,6 @@ describe('OAuth Clients API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
|
||||
CLIENT_0 = result.body;
|
||||
@@ -252,7 +239,6 @@ describe('OAuth Clients API', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(412);
|
||||
done();
|
||||
});
|
||||
@@ -267,7 +253,6 @@ describe('OAuth Clients API', function () {
|
||||
it('fails without token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -278,7 +263,6 @@ describe('OAuth Clients API', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
@@ -288,7 +272,6 @@ describe('OAuth Clients API', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body).to.eql(CLIENT_0);
|
||||
done();
|
||||
@@ -318,7 +301,6 @@ describe('OAuth Clients API', function () {
|
||||
.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(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
@@ -337,7 +319,6 @@ describe('OAuth Clients API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
|
||||
CLIENT_0 = result.body;
|
||||
@@ -359,7 +340,6 @@ describe('OAuth Clients API', function () {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(412);
|
||||
done();
|
||||
});
|
||||
@@ -374,7 +354,6 @@ describe('OAuth Clients API', function () {
|
||||
it('fails without token', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -385,7 +364,6 @@ describe('OAuth Clients API', function () {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
@@ -395,13 +373,11 @@ describe('OAuth Clients API', function () {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(404);
|
||||
|
||||
done();
|
||||
@@ -443,7 +419,6 @@ describe('Clients', function () {
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USER_0.username, password: USER_0.password, email: USER_0.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();
|
||||
@@ -473,7 +448,6 @@ describe('Clients', function () {
|
||||
it('fails due to missing token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -483,7 +457,6 @@ describe('Clients', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -493,7 +466,6 @@ describe('Clients', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -503,7 +475,6 @@ describe('Clients', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
expect(result.body.clients.length).to.eql(1);
|
||||
@@ -521,7 +492,6 @@ describe('Clients', function () {
|
||||
it('fails due to missing token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -531,7 +501,6 @@ describe('Clients', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -541,7 +510,6 @@ describe('Clients', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -551,7 +519,6 @@ describe('Clients', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/CID-WEBADMIN/tokens')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
@@ -561,7 +528,6 @@ describe('Clients', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
expect(result.body.tokens.length).to.eql(1);
|
||||
@@ -579,7 +545,6 @@ describe('Clients', function () {
|
||||
it('fails due to missing token', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -589,7 +554,6 @@ describe('Clients', function () {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -599,7 +563,6 @@ describe('Clients', function () {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -609,7 +572,6 @@ describe('Clients', function () {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/CID-WEBADMIN/tokens')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
@@ -619,7 +581,6 @@ describe('Clients', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
expect(result.body.tokens.length).to.eql(1);
|
||||
@@ -628,14 +589,12 @@ describe('Clients', function () {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
// further calls with this token should not work
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -10,12 +10,8 @@ var async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
nock = require('nock'),
|
||||
paths = require('../../paths.js'),
|
||||
request = require('superagent'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
shell = require('../../shell.js');
|
||||
|
||||
@@ -28,6 +24,7 @@ var server;
|
||||
function setup(done) {
|
||||
nock.cleanAll();
|
||||
config.set('version', '0.5.0');
|
||||
config.set('fqdn', 'localhost');
|
||||
server.start(done);
|
||||
}
|
||||
|
||||
@@ -57,10 +54,9 @@ describe('Cloudron', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('fails due to missing setupToken', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -69,11 +65,10 @@ describe('Cloudron', function () {
|
||||
it('fails due to empty username', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
@@ -83,11 +78,10 @@ describe('Cloudron', function () {
|
||||
it('fails due to empty password', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: '', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
@@ -97,11 +91,10 @@ describe('Cloudron', function () {
|
||||
it('fails due to empty email', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
@@ -111,11 +104,10 @@ describe('Cloudron', function () {
|
||||
it('fails due to empty name', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: '', email: 'admin@foo.bar', name: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
@@ -125,11 +117,10 @@ describe('Cloudron', function () {
|
||||
it('fails due to invalid email', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'invalidemail' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
@@ -140,11 +131,10 @@ describe('Cloudron', function () {
|
||||
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')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar', name: 'tester' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
@@ -155,11 +145,10 @@ describe('Cloudron', function () {
|
||||
it('fails the second time', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(409);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
@@ -167,101 +156,6 @@ describe('Cloudron', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Certificates API', function () {
|
||||
var certFile, keyFile;
|
||||
|
||||
before(function (done) {
|
||||
certFile = path.join(os.tmpdir(), 'host.cert');
|
||||
fs.writeFileSync(certFile, 'test certificate');
|
||||
|
||||
keyFile = path.join(os.tmpdir(), 'host.key');
|
||||
fs.writeFileSync(keyFile, 'test key');
|
||||
|
||||
async.series([
|
||||
setup,
|
||||
|
||||
function (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(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
], done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
fs.unlinkSync(certFile);
|
||||
fs.unlinkSync(keyFile);
|
||||
|
||||
cleanup(done);
|
||||
});
|
||||
|
||||
it('cannot set certificate without token', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/certificate')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set certificate without certificate', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/certificate')
|
||||
.query({ access_token: token })
|
||||
.attach('key', keyFile, 'key')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set certificate without key', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/certificate')
|
||||
.query({ access_token: token })
|
||||
.attach('certificate', certFile, 'certificate')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set certificate', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/certificate')
|
||||
.query({ access_token: token })
|
||||
.attach('key', keyFile, 'key')
|
||||
.attach('certificate', certFile, 'certificate')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('did set the certificate', function (done) {
|
||||
var cert = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'));
|
||||
expect(cert).to.eql(fs.readFileSync(certFile));
|
||||
|
||||
var key = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'));
|
||||
expect(key).to.eql(fs.readFileSync(keyFile));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get config', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
@@ -273,11 +167,10 @@ describe('Cloudron', function () {
|
||||
|
||||
config._reset();
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
superagent.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(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
@@ -294,30 +187,29 @@ describe('Cloudron', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('cannot get without token', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds without appstore', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
|
||||
expect(result.body.webServerOrigin).to.eql(null);
|
||||
expect(result.body.fqdn).to.eql('localhost');
|
||||
expect(result.body.fqdn).to.eql(config.fqdn());
|
||||
expect(result.body.isCustomDomain).to.eql(false);
|
||||
expect(result.body.progress).to.be.an('object');
|
||||
expect(result.body.update).to.be.an('object');
|
||||
expect(result.body.version).to.eql('0.5.0');
|
||||
expect(result.body.version).to.eql(config.version());
|
||||
expect(result.body.developerMode).to.be.a('boolean');
|
||||
expect(result.body.size).to.eql(null);
|
||||
expect(result.body.region).to.eql(null);
|
||||
expect(result.body.memory).to.eql(0);
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
|
||||
done();
|
||||
@@ -325,23 +217,23 @@ describe('Cloudron', function () {
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/localhost?token=' + config.token()).reply(200, { box: { region: 'sfo', size: 'small' }});
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/localhost?token=' + config.token()).reply(200, { box: { region: 'sfo', size: '1gb' }});
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
|
||||
expect(result.body.webServerOrigin).to.eql(null);
|
||||
expect(result.body.fqdn).to.eql('localhost');
|
||||
expect(result.body.fqdn).to.eql(config.fqdn());
|
||||
expect(result.body.isCustomDomain).to.eql(false);
|
||||
expect(result.body.progress).to.be.an('object');
|
||||
expect(result.body.update).to.be.an('object');
|
||||
expect(result.body.version).to.eql('0.5.0');
|
||||
expect(result.body.version).to.eql(config.version());
|
||||
expect(result.body.developerMode).to.be.a('boolean');
|
||||
expect(result.body.size).to.eql('small');
|
||||
expect(result.body.size).to.eql('1gb');
|
||||
expect(result.body.region).to.eql('sfo');
|
||||
expect(result.body.memory).to.eql(1073741824);
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -363,11 +255,10 @@ describe('Cloudron', function () {
|
||||
|
||||
config._reset();
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
superagent.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(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
@@ -378,72 +269,77 @@ describe('Cloudron', function () {
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function setupBackupConfig(callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/backup_config')
|
||||
.send({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo'})
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo'})
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with missing size', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with wrong size type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 4, region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with missing region', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('fails with wrong region type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 4, password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -451,11 +347,11 @@ describe('Cloudron', function () {
|
||||
|
||||
it('fails when in wrong state', function (done) {
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
|
||||
var scope3 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
|
||||
})
|
||||
.reply(200, { id: 'someid' });
|
||||
@@ -467,11 +363,10 @@ describe('Cloudron', function () {
|
||||
|
||||
injectShellMock();
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
@@ -487,29 +382,27 @@ describe('Cloudron', function () {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.size && body.region && body.restoreKey;
|
||||
}).reply(202, {});
|
||||
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
|
||||
})
|
||||
.reply(200, { id: 'someid' });
|
||||
|
||||
var scope3 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
|
||||
injectShellMock();
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
@@ -537,11 +430,10 @@ describe('Cloudron', function () {
|
||||
|
||||
config._reset();
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
superagent.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(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
@@ -558,125 +450,112 @@ describe('Cloudron', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
.send({ subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
.send({ type: '', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with unknown type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
.send({ type: 'foobar', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with ticket type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with app type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
.send({ type: 'app', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without description', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty subject', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
.send({ type: 'ticket', subject: '', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty description', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: '' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with feedback type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
.send({ type: 'feedback', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without subject', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
|
||||
.send({ type: 'ticket', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ var async = require('async'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
nock = require('nock'),
|
||||
request = require('superagent'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js');
|
||||
|
||||
@@ -43,11 +43,10 @@ describe('Developer API', function () {
|
||||
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')
|
||||
superagent.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(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
@@ -67,9 +66,8 @@ describe('Developer API', function () {
|
||||
settings.setDeveloperMode(true, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/developer')
|
||||
superagent.get(SERVER_URL + '/api/v1/developer')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -80,10 +78,9 @@ describe('Developer API', function () {
|
||||
settings.setDeveloperMode(true, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/developer')
|
||||
superagent.get(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
@@ -94,10 +91,9 @@ describe('Developer API', function () {
|
||||
settings.setDeveloperMode(false, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/developer')
|
||||
superagent.get(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(412);
|
||||
done();
|
||||
});
|
||||
@@ -114,11 +110,10 @@ describe('Developer API', function () {
|
||||
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')
|
||||
superagent.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(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
@@ -135,82 +130,74 @@ describe('Developer API', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer')
|
||||
.send({ enabled: true })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ enabled: true })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ password: '', enabled: true })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD.toUpperCase(), enabled: true })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing enabled property', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong enabled property type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, enabled: 'true' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds enabling', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, enabled: true })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/developer')
|
||||
superagent.get(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
@@ -218,17 +205,15 @@ describe('Developer API', function () {
|
||||
});
|
||||
|
||||
it('succeeds disabling', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, enabled: false })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/developer')
|
||||
superagent.get(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(412);
|
||||
done();
|
||||
});
|
||||
@@ -247,11 +232,10 @@ describe('Developer API', function () {
|
||||
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')
|
||||
superagent.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(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
@@ -268,79 +252,71 @@ describe('Developer API', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('fails without body', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without username', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: USERNAME })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty username', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: '', password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: USERNAME, password: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with unknown username', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: USERNAME.toUpperCase(), password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with wrong password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: USERNAME, password: PASSWORD.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('with username succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: USERNAME, password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.expiresAt).to.be.a('number');
|
||||
expect(result.body.token).to.be.a('string');
|
||||
@@ -349,10 +325,9 @@ describe('Developer API', function () {
|
||||
});
|
||||
|
||||
it('with email succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
superagent.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: EMAIL, password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.expiresAt).to.be.a('number');
|
||||
expect(result.body.token).to.be.a('string');
|
||||
|
||||
@@ -319,7 +319,6 @@ describe('OAuth2', function () {
|
||||
it('fails due to missing redirect_uri param', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
|
||||
expect(result.text.indexOf('Invalid request. redirect_uri query param is not set.')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
@@ -330,7 +329,6 @@ describe('OAuth2', function () {
|
||||
it('fails due to missing client_id param', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=http://someredirect')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
|
||||
expect(result.text.indexOf('Invalid request. client_id query param is not set.')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
@@ -341,7 +339,6 @@ describe('OAuth2', function () {
|
||||
it('fails due to missing response_type param', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=http://someredirect&client_id=someclientid')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
|
||||
expect(result.text.indexOf('Invalid request. response_type query param is not set.')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
@@ -352,7 +349,6 @@ describe('OAuth2', function () {
|
||||
it('fails for unkown grant type', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=http://someredirect&client_id=someclientid&response_type=foobar')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
|
||||
expect(result.text.indexOf('Invalid request. Only token and code response types are supported.')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
@@ -363,7 +359,6 @@ describe('OAuth2', function () {
|
||||
it('succeeds for grant type code', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=http://someredirect&client_id=someclientid&response_type=code')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text).to.eql('<script>window.location.href = "/api/v1/session/login?returnTo=http://someredirect";</script>');
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
@@ -373,7 +368,6 @@ describe('OAuth2', function () {
|
||||
it('succeeds for grant type token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=http://someredirect&client_id=someclientid&response_type=token')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text).to.eql('<script>window.location.href = "/api/v1/session/login?returnTo=http://someredirect";</script>');
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
@@ -388,7 +382,6 @@ describe('OAuth2', function () {
|
||||
it('fails without prior authentication call and not returnTo query', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/login')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
|
||||
expect(result.text.indexOf('Invalid login request. No returnTo provided.')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
@@ -401,7 +394,6 @@ describe('OAuth2', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/login?returnTo=http://someredirect')
|
||||
.redirects(0)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(302);
|
||||
expect(result.headers.location).to.eql('http://someredirect');
|
||||
|
||||
@@ -413,7 +405,6 @@ describe('OAuth2', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=http://someredirect&response_type=code')
|
||||
.redirects(0)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
|
||||
expect(result.text.indexOf('Invalid request. client_id query param is not set.')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
@@ -1289,7 +1280,6 @@ describe('Password', function () {
|
||||
it('reset request succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/resetRequest.html')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
@@ -1299,7 +1289,6 @@ describe('Password', function () {
|
||||
it('setup fails due to missing reset_token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/setup.html')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -1309,7 +1298,6 @@ describe('Password', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/setup.html')
|
||||
.query({ reset_token: hat(256) })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -1319,7 +1307,6 @@ describe('Password', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/setup.html')
|
||||
.query({ reset_token: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
|
||||
done();
|
||||
@@ -1329,7 +1316,6 @@ describe('Password', function () {
|
||||
it('reset fails due to missing reset_token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -1339,7 +1325,6 @@ describe('Password', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
|
||||
.query({ reset_token: hat(256) })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -1349,7 +1334,6 @@ describe('Password', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
|
||||
.query({ reset_token: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
@@ -1359,7 +1343,6 @@ describe('Password', function () {
|
||||
it('sent succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/sent.html')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
@@ -1375,7 +1358,6 @@ describe('Password', function () {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/resetRequest')
|
||||
.send({ identifier: USER_0.email })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
@@ -1391,7 +1373,6 @@ describe('Password', function () {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: 'somepassword' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -1401,7 +1382,6 @@ describe('Password', function () {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ resetToken: hat(256) })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
@@ -1411,7 +1391,6 @@ describe('Password', function () {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: '', resetToken: hat(256) })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -1421,7 +1400,6 @@ describe('Password', function () {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: '', resetToken: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -1439,7 +1417,6 @@ describe('Password', function () {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: 'somepassword', resetToken: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
|
||||
@@ -11,8 +11,9 @@ var appdb = require('../../appdb.js'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
path = require('path'),
|
||||
paths = require('../../paths.js'),
|
||||
request = require('superagent'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js'),
|
||||
fs = require('fs'),
|
||||
@@ -26,6 +27,8 @@ var token = null;
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
config.set('fqdn', 'foobar.com');
|
||||
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
|
||||
@@ -35,11 +38,10 @@ function setup(done) {
|
||||
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')
|
||||
superagent.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();
|
||||
@@ -75,17 +77,17 @@ describe('Settings API', function () {
|
||||
|
||||
describe('autoupdate_pattern', function () {
|
||||
it('can get auto update pattern (default)', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.pattern).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set autoupdate_pattern without pattern', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
@@ -99,7 +101,7 @@ describe('Settings API', function () {
|
||||
eventPattern = pattern;
|
||||
});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
.query({ access_token: token })
|
||||
.send({ pattern: '00 30 11 * * 1-5' })
|
||||
.end(function (err, res) {
|
||||
@@ -115,7 +117,7 @@ describe('Settings API', function () {
|
||||
eventPattern = pattern;
|
||||
});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
.query({ access_token: token })
|
||||
.send({ pattern: 'never' })
|
||||
.end(function (err, res) {
|
||||
@@ -126,7 +128,7 @@ describe('Settings API', function () {
|
||||
});
|
||||
|
||||
it('cannot set invalid autoupdate_pattern', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
.query({ access_token: token })
|
||||
.send({ pattern: '1 3 x 5 6' })
|
||||
.end(function (err, res) {
|
||||
@@ -140,17 +142,17 @@ describe('Settings API', function () {
|
||||
var name = 'foobar';
|
||||
|
||||
it('get default succeeds', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.name).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set without name', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
@@ -159,7 +161,7 @@ describe('Settings API', function () {
|
||||
});
|
||||
|
||||
it('cannot set empty name', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.send({ name: '' })
|
||||
.end(function (err, res) {
|
||||
@@ -169,7 +171,7 @@ describe('Settings API', function () {
|
||||
});
|
||||
|
||||
it('set succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.send({ name: name })
|
||||
.end(function (err, res) {
|
||||
@@ -179,29 +181,29 @@ describe('Settings API', function () {
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.name).to.eql(name);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloudron_avatar', function () {
|
||||
it('get default succeeds', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body).to.be.a(Buffer);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set without data', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
@@ -210,9 +212,9 @@ describe('Settings API', function () {
|
||||
});
|
||||
|
||||
it('set succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.attach('avatar', paths.FAVICON_FILE)
|
||||
.attach('avatar', paths.CLOUDRON_DEFAULT_AVATAR_FILE)
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
done();
|
||||
@@ -220,14 +222,143 @@ describe('Settings API', function () {
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.toString()).to.eql(fs.readFileSync(paths.FAVICON_FILE, 'utf-8'));
|
||||
expect(res.body.toString()).to.eql(fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE, 'utf-8'));
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dns_config', function () {
|
||||
it('get dns_config fails', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/dns_config')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body).to.eql({});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set without data', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/dns_config')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/dns_config')
|
||||
.query({ access_token: token })
|
||||
.send({ provider: 'route53', accessKeyId: 'accessKey', secretAccessKey: 'secretAccessKey' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/dns_config')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body).to.eql({ provider: 'route53', accessKeyId: 'accessKey', secretAccessKey: 'secretAccessKey', region: 'us-east-1', endpoint: null });
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Certificates API', function () {
|
||||
// foobar.com
|
||||
var validCert0 = '-----BEGIN CERTIFICATE-----\nMIIBujCCAWQCCQCjLyTKzAJ4FDANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzETMBEGA1UEAwwKZm9vYmFyLmNvbTAeFw0xNTEw\nMjgxMjM5MjZaFw0xNjEwMjcxMjM5MjZaMGQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQI\nDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UECgwHTmVidWxvbjEMMAoG\nA1UECwwDQ1RPMRMwEQYDVQQDDApmb29iYXIuY29tMFwwDQYJKoZIhvcNAQEBBQAD\nSwAwSAJBAMeYofgwHeNVmGkGe0gj4dnX2ciifDi7X2K/oVHp7mxuHjGMSYP9Z7b6\n+mu0IMf4OedwXStHBeO8mwjKxZmE7p8CAwEAATANBgkqhkiG9w0BAQsFAANBAJI7\nFUUHXjR63UFk8pgxp0c7hEGqj4VWWGsmo8oZnnX8jGVmQDKbk8o3MtDujfqupmMR\nMo7tSAFlG7zkm3GYhpw=\n-----END CERTIFICATE-----';
|
||||
var validKey0 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAMeYofgwHeNVmGkGe0gj4dnX2ciifDi7X2K/oVHp7mxuHjGMSYP9\nZ7b6+mu0IMf4OedwXStHBeO8mwjKxZmE7p8CAwEAAQJBAJS59Sb8o6i8JT9NJxvQ\nMQCkSJGqEaosZJ0uccSZ7aE48v+H7HiPzXAueitohcEif2Wp1EZ1RbRMURhznNiZ\neLECIQDxxqhakO6wc7H68zmpRXJ5ZxGUNbM24AMtpONAtEw9iwIhANNWtp6P74OV\ntvfOmtubbqw768fmGskFCOcp5oF8oF29AiBkTAf9AhCyjFwyAYJTEScq67HkLN66\njfVjkvpfFixmfwIgI+xldmZ5DCDyzQSthg7RrS0yUvRmMS1N6h1RNUl96PECIQDl\nit4lFcytbqNo1PuBZvzQE+plCjiJqXHYo3WCst1Jbg==\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
// *.foobar.com
|
||||
var validCert1 = '-----BEGIN CERTIFICATE-----\nMIIBvjCCAWgCCQCg957GWuHtbzANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEVMBMGA1UEAwwMKi5mb29iYXIuY29tMB4XDTE1\nMTAyODEzMDI1MFoXDTE2MTAyNzEzMDI1MFowZjELMAkGA1UEBhMCREUxDzANBgNV\nBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRAwDgYDVQQKDAdOZWJ1bG9uMQww\nCgYDVQQLDANDVE8xFTATBgNVBAMMDCouZm9vYmFyLmNvbTBcMA0GCSqGSIb3DQEB\nAQUAA0sAMEgCQQC0FKf07ZWMcABFlZw+GzXK9EiZrlJ1lpnu64RhN99z7MXRr8cF\nnZVgY3jgatuyR5s3WdzUvye2eJ0rNicl2EZJAgMBAAEwDQYJKoZIhvcNAQELBQAD\nQQAw4bteMZAeJWl2wgNLw+wTwAH96E0jyxwreCnT5AxJLmgimyQ0XOF4FsssdRFj\nxD9WA+rktelBodJyPeTDNhIh\n-----END CERTIFICATE-----';
|
||||
var validKey1 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBALQUp/TtlYxwAEWVnD4bNcr0SJmuUnWWme7rhGE333PsxdGvxwWd\nlWBjeOBq27JHmzdZ3NS/J7Z4nSs2JyXYRkkCAwEAAQJALV2eykcoC48TonQEPmkg\nbhaIS57syw67jMLsQImQ02UABKzqHPEKLXPOZhZPS9hsC/hGIehwiYCXMUlrl+WF\nAQIhAOntBI6qaecNjAAVG7UbZclMuHROUONmZUF1KNq6VyV5AiEAxRLkfHWy52CM\njOQrX347edZ30f4QczvugXwsyuU9A1ECIGlGZ8Sk4OBA8n6fAUcyO06qnmCJVlHg\npTUeOvKk5c9RAiBs28+8dCNbrbhVhx/yQr9FwNM0+ttJW/yWJ+pyNQhr0QIgJTT6\nxwCWYOtbioyt7B9l+ENy3AMSO3Uq+xmIKkvItK4=\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
it('cannot set certificate without token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set certificate without certificate', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
|
||||
.query({ access_token: token })
|
||||
.send({ key: validKey1 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set certificate without key', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
|
||||
.query({ access_token: token })
|
||||
.send({ cert: validCert1 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set certificate with cert not being a string', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
|
||||
.query({ access_token: token })
|
||||
.send({ cert: 1234, key: validKey1 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set certificate with key not being a string', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
|
||||
.query({ access_token: token })
|
||||
.send({ cert: validCert1, key: true })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set non wildcard certificate', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
|
||||
.query({ access_token: token })
|
||||
.send({ cert: validCert0, key: validKey0 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set certificate', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
|
||||
.query({ access_token: token })
|
||||
.send({ cert: validCert1, key: validKey1 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('did set the certificate', function (done) {
|
||||
var cert = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf-8');
|
||||
expect(cert).to.eql(validCert1);
|
||||
|
||||
var key = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf-8');
|
||||
expect(key).to.eql(validKey1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ var clientdb = require('../../clientdb.js'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
request = require('superagent'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
simpleauth = require('../../simpleauth.js'),
|
||||
nock = require('nock');
|
||||
@@ -109,7 +109,7 @@ describe('SimpleAuth API', function () {
|
||||
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')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
@@ -146,10 +146,9 @@ describe('SimpleAuth API', function () {
|
||||
it('cannot login without clientId', function (done) {
|
||||
var body = {};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.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();
|
||||
});
|
||||
@@ -160,10 +159,9 @@ describe('SimpleAuth API', function () {
|
||||
clientId: 'someclientid'
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.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();
|
||||
});
|
||||
@@ -175,10 +173,9 @@ describe('SimpleAuth API', function () {
|
||||
username: USERNAME
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.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();
|
||||
});
|
||||
@@ -191,10 +188,9 @@ describe('SimpleAuth API', function () {
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.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();
|
||||
});
|
||||
@@ -207,10 +203,9 @@ describe('SimpleAuth API', function () {
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.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();
|
||||
});
|
||||
@@ -223,10 +218,9 @@ describe('SimpleAuth API', function () {
|
||||
password: ''
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.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();
|
||||
});
|
||||
@@ -239,10 +233,9 @@ describe('SimpleAuth API', function () {
|
||||
password: PASSWORD+PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.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();
|
||||
});
|
||||
@@ -255,10 +248,9 @@ describe('SimpleAuth API', function () {
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.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();
|
||||
});
|
||||
@@ -271,10 +263,9 @@ describe('SimpleAuth API', function () {
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.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();
|
||||
});
|
||||
@@ -287,7 +278,7 @@ describe('SimpleAuth API', function () {
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
@@ -299,7 +290,7 @@ describe('SimpleAuth API', function () {
|
||||
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')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: result.body.accessToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
@@ -318,7 +309,7 @@ describe('SimpleAuth API', function () {
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
@@ -330,7 +321,7 @@ describe('SimpleAuth API', function () {
|
||||
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')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: result.body.accessToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
@@ -349,10 +340,9 @@ describe('SimpleAuth API', function () {
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.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();
|
||||
});
|
||||
@@ -369,7 +359,7 @@ describe('SimpleAuth API', function () {
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
@@ -382,35 +372,32 @@ describe('SimpleAuth API', function () {
|
||||
});
|
||||
|
||||
it('fails without access_token', function (done) {
|
||||
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
|
||||
superagent.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')
|
||||
superagent.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')
|
||||
superagent.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')
|
||||
superagent.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();
|
||||
|
||||
@@ -66,8 +66,6 @@ start_mongodb() {
|
||||
}
|
||||
|
||||
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" \
|
||||
|
||||
@@ -10,7 +10,7 @@ var config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
tokendb = require('../../tokendb.js'),
|
||||
expect = require('expect.js'),
|
||||
request = require('superagent'),
|
||||
superagent = require('superagent'),
|
||||
nock = require('nock'),
|
||||
server = require('../../server.js'),
|
||||
userdb = require('../../userdb.js');
|
||||
@@ -50,7 +50,7 @@ describe('User API', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('device is in first time mode', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/status')
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/status')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.activated).to.not.be.ok();
|
||||
@@ -61,21 +61,21 @@ describe('User API', function () {
|
||||
it('create admin fails due to missing parameters', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME_0 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create admin fails because only POST is allowed', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('User API', function () {
|
||||
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')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME_0, password: PASSWORD, email: EMAIL })
|
||||
.end(function (err, res) {
|
||||
@@ -99,16 +99,16 @@ describe('User API', function () {
|
||||
});
|
||||
|
||||
it('device left first time mode', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/status')
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/status')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.activated).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get userInfo with token', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -119,7 +119,7 @@ describe('User API', function () {
|
||||
// stash for further use
|
||||
user_0 = res.body;
|
||||
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,10 +131,9 @@ describe('User API', function () {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
setTimeout(function () {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + user_0.username)
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + user_0.username)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
@@ -143,46 +142,46 @@ describe('User API', function () {
|
||||
});
|
||||
|
||||
it('can get userInfo with token', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0);
|
||||
expect(res.body.email).to.equal(EMAIL);
|
||||
expect(res.body.admin).to.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get userInfo only with basic auth', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.auth(USERNAME_0, PASSWORD)
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get userInfo with invalid token (token length)', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: 'x' + token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get userInfo with invalid token (wrong token)', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get userInfo with token in auth header', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.set('Authorization', 'Bearer ' + token)
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -191,30 +190,30 @@ describe('User API', function () {
|
||||
expect(res.body.admin).to.be.ok();
|
||||
expect(res.body.password).to.not.be.ok();
|
||||
expect(res.body.salt).to.not.be.ok();
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get userInfo with invalid token in auth header', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.set('Authorization', 'Bearer ' + 'x' + token)
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get userInfo with invalid token (wrong token)', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.set('Authorization', 'Bearer ' + 'x' + token.toUpperCase())
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create second user succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1 })
|
||||
.end(function (err, res) {
|
||||
@@ -228,90 +227,86 @@ describe('User API', function () {
|
||||
|
||||
it('set second user as admin succeeds', function (done) {
|
||||
// TODO is USERNAME_1 in body and url redundant?
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, admin: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('remove first user from admins succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ username: USERNAME_0, admin: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('remove second user by first, now normal, user fails', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('remove second user from admins and thus last admin fails', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ username: USERNAME_1, admin: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('reset first user as admin succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ username: USERNAME_0, admin: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create user missing username fails', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ email: EMAIL_2 })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create user missing email fails', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2 })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create second and third user', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2, email: EMAIL_2 })
|
||||
.end(function (error, res) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_3, email: EMAIL_3 })
|
||||
.end(function (error, res) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
@@ -321,10 +316,9 @@ describe('User API', function () {
|
||||
});
|
||||
|
||||
it('second user userInfo', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.username).to.equal(USERNAME_2);
|
||||
expect(result.body.email).to.equal(EMAIL_2);
|
||||
@@ -335,17 +329,17 @@ describe('User API', function () {
|
||||
});
|
||||
|
||||
it('create user with same username should fail', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2, email: EMAIL })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(409);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('list users', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users')
|
||||
superagent.get(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token_2 })
|
||||
.end(function (error, res) {
|
||||
expect(error).to.be(null);
|
||||
@@ -367,106 +361,106 @@ describe('User API', function () {
|
||||
});
|
||||
|
||||
it('user removes himself is not allowed', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('admin cannot remove normal user without giving a password', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('admin cannot remove normal user with empty password', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
.query({ access_token: token })
|
||||
.send({ password: '' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('admin cannot remove normal user with giving wrong password', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD + PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('admin removes normal user', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('admin removes himself should not be allowed', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// Change email
|
||||
it('change email fails due to missing token', function (done) {
|
||||
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.send({ password: PASSWORD, email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change email fails due to missing password', function (done) {
|
||||
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change email fails due to wrong password', function (done) {
|
||||
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD+PASSWORD, email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(403);
|
||||
done(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change email fails due to invalid email', function (done) {
|
||||
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, email: 'foo@bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change email succeeds', function (done) {
|
||||
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
@@ -477,52 +471,52 @@ describe('User API', function () {
|
||||
|
||||
// Change password
|
||||
it('change password fails due to missing current password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ newPassword: 'some wrong password' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change password fails due to missing new password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change password fails due to wrong password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: 'some wrong password', newPassword: 'newpassword' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change password fails due to invalid password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, newPassword: 'five' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change password succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, newPassword: 'new_password' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ var appdb = require('./appdb.js'),
|
||||
safe = require('safetydance'),
|
||||
_ = require('underscore');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug('Unhandled error: ', error); };
|
||||
|
||||
// appId -> { schedulerConfig (manifest), cronjobs, containerIds }
|
||||
var gState = (function loadState() {
|
||||
@@ -180,7 +180,10 @@ function doTask(appId, taskName, callback) {
|
||||
|
||||
debug('Creating createSubcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
|
||||
|
||||
docker.createSubcontainer(app, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], function (error, container) {
|
||||
// NOTE: if you change container name here, fix addons.js to return correct container names
|
||||
docker.createSubcontainer(app, app.id + '-' + taskName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], { } /* options */, function (error, container) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appState.containerIds[taskName] = container.id;
|
||||
|
||||
saveState(gState);
|
||||
|
||||
@@ -10,6 +10,7 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
auth = require('./auth.js'),
|
||||
certificates = require('./certificates.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
cron = require('./cron.js'),
|
||||
config = require('./config.js'),
|
||||
@@ -20,7 +21,6 @@ var assert = require('assert'),
|
||||
middleware = require('./middleware'),
|
||||
passport = require('passport'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
routes = require('./routes/index.js'),
|
||||
taskmanager = require('./taskmanager.js');
|
||||
|
||||
@@ -53,7 +53,6 @@ function initializeExpressSync() {
|
||||
.use(json)
|
||||
.use(urlencoded)
|
||||
.use(middleware.cookieParser())
|
||||
.use(middleware.favicon(paths.FAVICON_FILE)) // used when serving oauth login page
|
||||
.use(middleware.cors({ origins: [ '*' ], allowCredentials: true }))
|
||||
.use(middleware.session({ secret: 'yellow is blue', resave: true, saveUninitialized: true, cookie: { path: '/', httpOnly: true, secure: false, maxAge: 600000 } }))
|
||||
.use(passport.initialize())
|
||||
@@ -95,7 +94,6 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/cloudron/update', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
|
||||
router.post('/api/v1/cloudron/reboot', rootScope, routes.cloudron.reboot);
|
||||
router.post('/api/v1/cloudron/migrate', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
|
||||
router.post('/api/v1/cloudron/certificate', rootScope, multipart, routes.cloudron.setCertificate);
|
||||
router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
|
||||
|
||||
// feedback
|
||||
@@ -161,6 +159,12 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.settings.setCloudronName);
|
||||
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.settings.getCloudronAvatar);
|
||||
router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar);
|
||||
router.get ('/api/v1/settings/dns_config', settingsScope, routes.settings.getDnsConfig);
|
||||
router.post('/api/v1/settings/dns_config', settingsScope, routes.settings.setDnsConfig);
|
||||
router.get ('/api/v1/settings/backup_config', settingsScope, routes.settings.getBackupConfig);
|
||||
router.post('/api/v1/settings/backup_config', settingsScope, routes.settings.setBackupConfig);
|
||||
router.post('/api/v1/settings/certificate', settingsScope, routes.settings.setCertificate);
|
||||
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.settings.setAdminCertificate);
|
||||
|
||||
// backup routes
|
||||
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
|
||||
@@ -231,8 +235,9 @@ function start(callback) {
|
||||
async.series([
|
||||
auth.initialize,
|
||||
database.initialize,
|
||||
cloudron.initialize, // keep this here because it reads activation state that others depend on
|
||||
certificates.installAdminCertificate, // keep this before cron to block heartbeats until cert is ready
|
||||
taskmanager.initialize,
|
||||
cloudron.initialize,
|
||||
mailer.initialize,
|
||||
cron.initialize,
|
||||
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
|
||||
|
||||
@@ -20,6 +20,15 @@ exports = module.exports = {
|
||||
getDeveloperMode: getDeveloperMode,
|
||||
setDeveloperMode: setDeveloperMode,
|
||||
|
||||
getDnsConfig: getDnsConfig,
|
||||
setDnsConfig: setDnsConfig,
|
||||
|
||||
getBackupConfig: getBackupConfig,
|
||||
setBackupConfig: setBackupConfig,
|
||||
|
||||
getTlsConfig: getTlsConfig,
|
||||
setTlsConfig: setTlsConfig,
|
||||
|
||||
getDefaultSync: getDefaultSync,
|
||||
getAll: getAll,
|
||||
|
||||
@@ -27,6 +36,9 @@ exports = module.exports = {
|
||||
TIME_ZONE_KEY: 'time_zone',
|
||||
CLOUDRON_NAME_KEY: 'cloudron_name',
|
||||
DEVELOPER_MODE_KEY: 'developer_mode',
|
||||
DNS_CONFIG_KEY: 'dns_config',
|
||||
BACKUP_CONFIG_KEY: 'backup_config',
|
||||
TLS_CONFIG_KEY: 'tls_config',
|
||||
|
||||
events: new (require('events').EventEmitter)()
|
||||
};
|
||||
@@ -47,6 +59,9 @@ var gDefaults = (function () {
|
||||
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
|
||||
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
|
||||
result[exports.DEVELOPER_MODE_KEY] = false;
|
||||
result[exports.DNS_CONFIG_KEY] = { };
|
||||
result[exports.BACKUP_CONFIG_KEY] = { };
|
||||
result[exports.TLS_CONFIG_KEY] = { provider: 'caas' };
|
||||
|
||||
return result;
|
||||
})();
|
||||
@@ -207,6 +222,107 @@ function setDeveloperMode(enabled, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getDnsConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.DNS_CONFIG_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.DNS_CONFIG_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, JSON.parse(value)); // accessKeyId, secretAccessKey, region
|
||||
});
|
||||
}
|
||||
|
||||
function setDnsConfig(dnsConfig, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var credentials;
|
||||
|
||||
if (dnsConfig.provider === 'route53') {
|
||||
if (typeof dnsConfig.accessKeyId !== 'string') return callback(new SettingsError(SettingsError.BAD_FIELD, 'accessKeyId must be a string'));
|
||||
if (typeof dnsConfig.secretAccessKey !== 'string') return callback(new SettingsError(SettingsError.BAD_FIELD, 'secretAccessKey must be a string'));
|
||||
|
||||
credentials = {
|
||||
provider: dnsConfig.provider,
|
||||
accessKeyId: dnsConfig.accessKeyId,
|
||||
secretAccessKey: dnsConfig.secretAccessKey,
|
||||
region: dnsConfig.region || 'us-east-1',
|
||||
endpoint: dnsConfig.endpoint || null
|
||||
};
|
||||
} else if (dnsConfig.provider === 'caas') {
|
||||
credentials = {
|
||||
provider: dnsConfig.provider
|
||||
};
|
||||
} else {
|
||||
return callback(new SettingsError(SettingsError.BAD_FIELD, 'provider must be route53 or caas'));
|
||||
}
|
||||
|
||||
settingsdb.set(exports.DNS_CONFIG_KEY, JSON.stringify(credentials), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.DNS_CONFIG_KEY, dnsConfig);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getTlsConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.TLS_CONFIG_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.TLS_CONFIG_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, JSON.parse(value)); // provider
|
||||
});
|
||||
}
|
||||
|
||||
function setTlsConfig(tlsConfig, callback) {
|
||||
assert.strictEqual(typeof tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('le-') !== 0) {
|
||||
return callback(new SettingsError(SettingsError.BAD_FIELD, 'provider must be caas or le-*'));
|
||||
}
|
||||
|
||||
settingsdb.set(exports.TLS_CONFIG_KEY, JSON.stringify(tlsConfig), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.TLS_CONFIG_KEY, tlsConfig);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getBackupConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.BACKUP_CONFIG_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.BACKUP_CONFIG_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, JSON.parse(value)); // provider, token, key, region, prefix, bucket
|
||||
});
|
||||
}
|
||||
|
||||
function setBackupConfig(backupConfig, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (backupConfig.provider !== 'caas') {
|
||||
return callback(new SettingsError(SettingsError.BAD_FIELD, 'provider must be caas'));
|
||||
}
|
||||
|
||||
settingsdb.set(exports.BACKUP_CONFIG_KEY, JSON.stringify(backupConfig), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.BACKUP_CONFIG_KEY, backupConfig);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getDefaultSync(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getSignedUploadUrl: getSignedUploadUrl,
|
||||
getSignedDownloadUrl: getSignedDownloadUrl,
|
||||
|
||||
copyObject: copyObject,
|
||||
|
||||
getAllPaged: getAllPaged
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
AWS = require('aws-sdk'),
|
||||
config = require('../config.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
function getBackupCredentials(backupConfig, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(backupConfig.token);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
|
||||
superagent.post(url).query({ token: backupConfig.token }).end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 201) return callback(new Error(result.text));
|
||||
if (!result.body || !result.body.credentials) return callback(new Error('Unexpected response'));
|
||||
|
||||
var credentials = {
|
||||
accessKeyId: result.body.credentials.AccessKeyId,
|
||||
secretAccessKey: result.body.credentials.SecretAccessKey,
|
||||
sessionToken: result.body.credentials.SessionToken,
|
||||
region: 'us-east-1'
|
||||
};
|
||||
|
||||
if (backupConfig.endpoint) credentials.endpoint = new AWS.Endpoint(backupConfig.endpoint);
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
}
|
||||
|
||||
function getAllPaged(backupConfig, page, perPage, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backups';
|
||||
superagent.get(url).query({ token: backupConfig.token }).end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new Error(result.text));
|
||||
if (!result.body || !util.isArray(result.body.backups)) return callback(new Error('Unexpected response'));
|
||||
|
||||
// [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first)
|
||||
return callback(null, result.body.backups);
|
||||
});
|
||||
}
|
||||
|
||||
function getSignedUploadUrl(backupConfig, filename, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof filename, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!backupConfig.bucket || !backupConfig.prefix) return new Error('Invalid configuration'); // prevent error in s3
|
||||
|
||||
getBackupCredentials(backupConfig, function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
|
||||
var params = {
|
||||
Bucket: backupConfig.bucket,
|
||||
Key: backupConfig.prefix + '/' + filename,
|
||||
Expires: 60 * 30 /* 30 minutes */
|
||||
};
|
||||
|
||||
var url = s3.getSignedUrl('putObject', params);
|
||||
|
||||
callback(null, { url : url, sessionToken: credentials.sessionToken });
|
||||
});
|
||||
}
|
||||
|
||||
function getSignedDownloadUrl(backupConfig, filename, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof filename, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!backupConfig.bucket || !backupConfig.prefix) return new Error('Invalid configuration'); // prevent error in s3
|
||||
|
||||
getBackupCredentials(backupConfig, function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
|
||||
var params = {
|
||||
Bucket: backupConfig.bucket,
|
||||
Key: backupConfig.prefix + '/' + filename,
|
||||
Expires: 60 * 30 /* 30 minutes */
|
||||
};
|
||||
|
||||
var url = s3.getSignedUrl('getObject', params);
|
||||
|
||||
callback(null, { url: url, sessionToken: credentials.sessionToken });
|
||||
});
|
||||
}
|
||||
|
||||
function copyObject(backupConfig, from, to, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof from, 'string');
|
||||
assert.strictEqual(typeof to, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!backupConfig.bucket || !backupConfig.prefix) return new Error('Invalid configuration'); // prevent error in s3
|
||||
|
||||
getBackupCredentials(backupConfig, function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var params = {
|
||||
Bucket: backupConfig.bucket, // target bucket
|
||||
Key: backupConfig.prefix + '/' + to, // target file
|
||||
CopySource: backupConfig.bucket + '/' + backupConfig.prefix + '/' + from, // source
|
||||
};
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
s3.copyObject(params, callback);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getSignedUploadUrl: getSignedUploadUrl,
|
||||
getSignedDownloadUrl: getSignedDownloadUrl,
|
||||
|
||||
copyObject: copyObject,
|
||||
getAllPaged: getAllPaged
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
AWS = require('aws-sdk');
|
||||
|
||||
function getBackupCredentials(backupConfig, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
assert(backupConfig.accessKeyId && backupConfig.secretAccessKey);
|
||||
|
||||
var credentials = {
|
||||
accessKeyId: backupConfig.accessKeyId,
|
||||
secretAccessKey: backupConfig.secretAccessKey,
|
||||
region: 'us-east-1'
|
||||
};
|
||||
|
||||
if (backupConfig.endpoint) credentials.endpoint = new AWS.Endpoint(backupConfig.endpoint);
|
||||
|
||||
callback(null, credentials);
|
||||
}
|
||||
|
||||
function getAllPaged(backupConfig, page, perPage, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
return callback(new Error('Not implemented yet'));
|
||||
}
|
||||
|
||||
function getSignedUploadUrl(backupConfig, filename, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof filename, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getBackupCredentials(backupConfig, function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
|
||||
var params = {
|
||||
Bucket: backupConfig.bucket,
|
||||
Key: backupConfig.prefix + '/' + filename,
|
||||
Expires: 60 * 30 /* 30 minutes */
|
||||
};
|
||||
|
||||
var url = s3.getSignedUrl('putObject', params);
|
||||
|
||||
callback(null, { url : url, sessionToken: credentials.sessionToken });
|
||||
});
|
||||
}
|
||||
|
||||
function getSignedDownloadUrl(backupConfig, filename, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof filename, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getBackupCredentials(backupConfig, function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
|
||||
var params = {
|
||||
Bucket: backupConfig.bucket,
|
||||
Key: backupConfig.prefix + '/' + filename,
|
||||
Expires: 60 * 30 /* 30 minutes */
|
||||
};
|
||||
|
||||
var url = s3.getSignedUrl('getObject', params);
|
||||
|
||||
callback(null, { url: url, sessionToken: credentials.sessionToken });
|
||||
});
|
||||
}
|
||||
|
||||
function copyObject(backupConfig, from, to, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof from, 'string');
|
||||
assert.strictEqual(typeof to, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getBackupCredentials(backupConfig, function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var params = {
|
||||
Bucket: backupConfig.bucket, // target bucket
|
||||
Key: backupConfig.prefix + '/' + to, // target file
|
||||
CopySource: backupConfig.bucket + '/' + backupConfig.prefix + '/' + from, // source
|
||||
};
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
s3.copyObject(params, callback);
|
||||
});
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = SubdomainError;
|
||||
|
||||
function SubdomainError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(SubdomainError, Error);
|
||||
|
||||
SubdomainError.NOT_FOUND = 'No such domain';
|
||||
SubdomainError.EXTERNAL_ERROR = 'External error';
|
||||
SubdomainError.STILL_BUSY = 'Still busy';
|
||||
SubdomainError.MISSING_CREDENTIALS = 'Missing credentials';
|
||||
@@ -2,77 +2,123 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
aws = require('./aws.js'),
|
||||
caas = require('./caas.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:subdomains'),
|
||||
util = require('util'),
|
||||
SubdomainError = require('./subdomainerror.js');
|
||||
|
||||
module.exports = exports = {
|
||||
add: add,
|
||||
addMany: addMany,
|
||||
remove: remove,
|
||||
status: status
|
||||
status: status,
|
||||
update: update, // unlike add, this fetches latest value, compares and adds if necessary. atomicity depends on backend
|
||||
get: get,
|
||||
|
||||
SubdomainError: SubdomainError
|
||||
};
|
||||
|
||||
// choose which subdomain backend we use
|
||||
// for test purpose we use aws
|
||||
function api() {
|
||||
return config.token() && !config.TEST ? caas : aws;
|
||||
var assert = require('assert'),
|
||||
caas = require('./dns/caas.js'),
|
||||
config = require('./config.js'),
|
||||
route53 = require('./dns/route53.js'),
|
||||
settings = require('./settings.js'),
|
||||
util = require('util');
|
||||
|
||||
function SubdomainError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(SubdomainError, Error);
|
||||
|
||||
SubdomainError.NOT_FOUND = 'No such domain';
|
||||
SubdomainError.EXTERNAL_ERROR = 'External error';
|
||||
SubdomainError.STILL_BUSY = 'Still busy';
|
||||
SubdomainError.MISSING_CREDENTIALS = 'Missing credentials';
|
||||
SubdomainError.INTERNAL_ERROR = 'Missing credentials';
|
||||
|
||||
// choose which subdomain backend we use for test purpose we use route53
|
||||
function api(provider) {
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
|
||||
switch (provider) {
|
||||
case 'caas': return caas;
|
||||
case 'route53': return route53;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
function add(record, callback) {
|
||||
assert.strictEqual(typeof record, 'object');
|
||||
assert.strictEqual(typeof record.subdomain, 'string');
|
||||
assert.strictEqual(typeof record.type, 'string');
|
||||
assert.strictEqual(typeof record.value, 'string');
|
||||
function add(subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('add: ', record);
|
||||
settings.getDnsConfig(function (error, dnsConfig) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
|
||||
|
||||
api().addSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
callback(null, changeId);
|
||||
api(dnsConfig.provider).add(dnsConfig, config.zoneName(), subdomain, type, values, function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
callback(null, changeId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addMany(records, callback) {
|
||||
assert(util.isArray(records));
|
||||
function get(subdomain, type, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('addMany: ', records);
|
||||
settings.getDnsConfig(function (error, dnsConfig) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
|
||||
|
||||
var changeIds = [];
|
||||
|
||||
async.eachSeries(records, function (record, callback) {
|
||||
add(record, function (error, changeId) {
|
||||
api(dnsConfig.provider).get(dnsConfig, config.zoneName(), subdomain, type, function (error, values) {
|
||||
if (error) return callback(error);
|
||||
|
||||
changeIds.push(changeId);
|
||||
callback(null, values);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function update(subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getDnsConfig(function (error, dnsConfig) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
|
||||
|
||||
api(dnsConfig.provider).update(dnsConfig, config.zoneName(), subdomain, type, values, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
callback(null, changeIds);
|
||||
});
|
||||
}
|
||||
|
||||
function remove(record, callback) {
|
||||
assert.strictEqual(typeof record, 'object');
|
||||
function remove(subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('remove: ', record);
|
||||
settings.getDnsConfig(function (error, dnsConfig) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
|
||||
|
||||
api().delSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error) {
|
||||
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
|
||||
api(dnsConfig.provider).del(dnsConfig, config.zoneName(), subdomain, type, values, function (error) {
|
||||
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
|
||||
|
||||
debug('deleteSubdomain: successfully deleted subdomain from aws.');
|
||||
|
||||
callback(null);
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,8 +126,12 @@ function status(changeId, callback) {
|
||||
assert.strictEqual(typeof changeId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
api().getChangeStatus(changeId, function (error, status) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
|
||||
callback(null, status === 'INSYNC' ? 'done' : 'pending');
|
||||
settings.getDnsConfig(function (error, dnsConfig) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
|
||||
|
||||
api(dnsConfig.provider).getChangeStatus(dnsConfig, changeId, function (error, status) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
|
||||
callback(null, status === 'INSYNC' ? 'done' : 'pending');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ exports = module.exports = {
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
child_process = require('child_process'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
debug = require('debug')('box:taskmanager'),
|
||||
locker = require('./locker.js'),
|
||||
_ = require('underscore');
|
||||
@@ -18,12 +20,38 @@ var gActiveTasks = { };
|
||||
var gPendingTasks = [ ];
|
||||
|
||||
var TASK_CONCURRENCY = 5;
|
||||
var NOOP_CALLBACK = function (error) { console.error(error); };
|
||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// resume app installs and uninstalls
|
||||
locker.on('unlocked', startNextTask);
|
||||
|
||||
if (cloudron.isConfiguredSync()) {
|
||||
resumeTasks();
|
||||
} else {
|
||||
cloudron.events.on(cloudron.EVENT_CONFIGURED, resumeTasks);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gPendingTasks = [ ]; // clear this first, otherwise stopAppTask will resume them
|
||||
|
||||
cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, resumeTasks);
|
||||
locker.removeListener('unlocked', startNextTask);
|
||||
|
||||
async.eachSeries(Object.keys(gActiveTasks), stopAppTask, callback);
|
||||
}
|
||||
|
||||
|
||||
// resume app installs and uninstalls
|
||||
function resumeTasks(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -36,21 +64,6 @@ function initialize(callback) {
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
locker.on('unlocked', startNextTask);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gPendingTasks = [ ]; // clear this first, otherwise stopAppTask will resume them
|
||||
for (var appId in gActiveTasks) {
|
||||
stopAppTask(appId);
|
||||
}
|
||||
|
||||
locker.removeListener('unlocked', startNextTask);
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function startNextTask() {
|
||||
@@ -80,31 +93,47 @@ function startAppTask(appId) {
|
||||
}
|
||||
|
||||
gActiveTasks[appId] = child_process.fork(__dirname + '/apptask.js', [ appId ]);
|
||||
gActiveTasks[appId].once('exit', function (code) {
|
||||
debug('Task for %s completed with status %s', appId, code);
|
||||
if (code && code !== 50) { // apptask crashed
|
||||
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code }, NOOP_CALLBACK);
|
||||
|
||||
var pid = gActiveTasks[appId].pid;
|
||||
debug('Started task of %s pid: %s', appId, pid);
|
||||
|
||||
gActiveTasks[appId].once('exit', function (code, signal) {
|
||||
debug('Task for %s pid %s completed with status %s', appId, pid, code);
|
||||
if (code === null /* signal */ || (code !== 0 && code !== 50)) { // apptask crashed
|
||||
debug('Apptask crashed with code %s and signal %s', code, signal);
|
||||
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code + ' and signal ' + signal }, NOOP_CALLBACK);
|
||||
}
|
||||
delete gActiveTasks[appId];
|
||||
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
|
||||
});
|
||||
}
|
||||
|
||||
function stopAppTask(appId) {
|
||||
function stopAppTask(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gActiveTasks[appId]) {
|
||||
debug('stopAppTask : Killing existing task of %s with pid %s: ', appId, gActiveTasks[appId].pid);
|
||||
debug('stopAppTask : Killing existing task of %s with pid %s', appId, gActiveTasks[appId].pid);
|
||||
gActiveTasks[appId].once('exit', function () { callback(); });
|
||||
gActiveTasks[appId].kill(); // this will end up calling the 'exit' handler
|
||||
delete gActiveTasks[appId];
|
||||
} else if (gPendingTasks.indexOf(appId) !== -1) {
|
||||
debug('stopAppTask: Removing existing pending task : %s', appId);
|
||||
gPendingTasks = _.without(gPendingTasks, appId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (gPendingTasks.indexOf(appId) !== -1) {
|
||||
debug('stopAppTask: Removing pending task : %s', appId);
|
||||
gPendingTasks = _.without(gPendingTasks, appId);
|
||||
} else {
|
||||
debug('stopAppTask: no task for %s to be stopped', appId);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function restartAppTask(appId) {
|
||||
stopAppTask(appId);
|
||||
startAppTask(appId);
|
||||
}
|
||||
function restartAppTask(appId, callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
async.series([
|
||||
stopAppTask.bind(null, appId),
|
||||
startAppTask.bind(null, appId)
|
||||
], callback);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
var addons = require('../addons.js'),
|
||||
appdb = require('../appdb.js'),
|
||||
apptask = require('../apptask.js'),
|
||||
async = require('async'),
|
||||
config = require('../config.js'),
|
||||
database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
@@ -17,6 +18,7 @@ var addons = require('../addons.js'),
|
||||
net = require('net'),
|
||||
nock = require('nock'),
|
||||
paths = require('../paths.js'),
|
||||
settings = require('../settings.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
var MANIFEST = {
|
||||
@@ -80,10 +82,12 @@ var APP = {
|
||||
describe('apptask', function () {
|
||||
before(function (done) {
|
||||
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, APP.oauthProxy, done);
|
||||
});
|
||||
async.series([
|
||||
database.initialize,
|
||||
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.oauthProxy),
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
|
||||
settings.setTlsConfig.bind(null, { provider: 'caas' })
|
||||
], done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
@@ -94,12 +98,12 @@ describe('apptask', function () {
|
||||
apptask.initialize(done);
|
||||
});
|
||||
|
||||
it('free port', function (done) {
|
||||
apptask._getFreePort(function (error, port) {
|
||||
expect(error).to.be(null);
|
||||
expect(port).to.be.a('number');
|
||||
var client = net.connect(port);
|
||||
client.on('connect', function () { done(new Error('Port is not free:' + port)); });
|
||||
it('reserve port', function (done) {
|
||||
apptask._reserveHttpPort(APP, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(APP.httpPort).to.be.a('number');
|
||||
var client = net.connect(APP.httpPort);
|
||||
client.on('connect', function () { done(new Error('Port is not free:' + APP.httpPort)); });
|
||||
client.on('error', function (error) { done(); });
|
||||
});
|
||||
});
|
||||
@@ -200,12 +204,8 @@ describe('apptask', function () {
|
||||
|
||||
it('registers subdomain', function (done) {
|
||||
nock.cleanAll();
|
||||
var scope = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
|
||||
.times(2)
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
|
||||
var awsScope = nock(config.aws().endpoint)
|
||||
var awsScope = nock('http://localhost:5353')
|
||||
.get('/2013-04-01/hostedzone')
|
||||
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }))
|
||||
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
|
||||
@@ -213,7 +213,6 @@ describe('apptask', function () {
|
||||
|
||||
apptask._registerSubdomain(APP, function (error) {
|
||||
expect(error).to.be(null);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(awsScope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
@@ -221,12 +220,8 @@ describe('apptask', function () {
|
||||
|
||||
it('unregisters subdomain', function (done) {
|
||||
nock.cleanAll();
|
||||
var scope = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
|
||||
.times(2)
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
|
||||
var awsScope = nock(config.aws().endpoint)
|
||||
var awsScope = nock('http://localhost:5353')
|
||||
.get('/2013-04-01/hostedzone')
|
||||
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }))
|
||||
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
|
||||
@@ -234,7 +229,6 @@ describe('apptask', function () {
|
||||
|
||||
apptask._unregisterSubdomain(APP, APP.location, function (error) {
|
||||
expect(error).to.be(null);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(awsScope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var certificates = require('../certificates.js'),
|
||||
expect = require('expect.js');
|
||||
|
||||
describe('Certificates', function () {
|
||||
describe('validateCertificate', function () {
|
||||
/*
|
||||
Generate these with:
|
||||
openssl genrsa -out server.key 512
|
||||
openssl req -new -key server.key -out server.csr -subj "/C=DE/ST=Berlin/L=Berlin/O=Nebulon/OU=CTO/CN=baz.foobar.com"
|
||||
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
|
||||
*/
|
||||
|
||||
// foobar.com
|
||||
var validCert0 = '-----BEGIN CERTIFICATE-----\nMIIBujCCAWQCCQCjLyTKzAJ4FDANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzETMBEGA1UEAwwKZm9vYmFyLmNvbTAeFw0xNTEw\nMjgxMjM5MjZaFw0xNjEwMjcxMjM5MjZaMGQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQI\nDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UECgwHTmVidWxvbjEMMAoG\nA1UECwwDQ1RPMRMwEQYDVQQDDApmb29iYXIuY29tMFwwDQYJKoZIhvcNAQEBBQAD\nSwAwSAJBAMeYofgwHeNVmGkGe0gj4dnX2ciifDi7X2K/oVHp7mxuHjGMSYP9Z7b6\n+mu0IMf4OedwXStHBeO8mwjKxZmE7p8CAwEAATANBgkqhkiG9w0BAQsFAANBAJI7\nFUUHXjR63UFk8pgxp0c7hEGqj4VWWGsmo8oZnnX8jGVmQDKbk8o3MtDujfqupmMR\nMo7tSAFlG7zkm3GYhpw=\n-----END CERTIFICATE-----';
|
||||
var validKey0 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAMeYofgwHeNVmGkGe0gj4dnX2ciifDi7X2K/oVHp7mxuHjGMSYP9\nZ7b6+mu0IMf4OedwXStHBeO8mwjKxZmE7p8CAwEAAQJBAJS59Sb8o6i8JT9NJxvQ\nMQCkSJGqEaosZJ0uccSZ7aE48v+H7HiPzXAueitohcEif2Wp1EZ1RbRMURhznNiZ\neLECIQDxxqhakO6wc7H68zmpRXJ5ZxGUNbM24AMtpONAtEw9iwIhANNWtp6P74OV\ntvfOmtubbqw768fmGskFCOcp5oF8oF29AiBkTAf9AhCyjFwyAYJTEScq67HkLN66\njfVjkvpfFixmfwIgI+xldmZ5DCDyzQSthg7RrS0yUvRmMS1N6h1RNUl96PECIQDl\nit4lFcytbqNo1PuBZvzQE+plCjiJqXHYo3WCst1Jbg==\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
// *.foobar.com
|
||||
var validCert1 = '-----BEGIN CERTIFICATE-----\nMIIBvjCCAWgCCQCg957GWuHtbzANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEVMBMGA1UEAwwMKi5mb29iYXIuY29tMB4XDTE1\nMTAyODEzMDI1MFoXDTE2MTAyNzEzMDI1MFowZjELMAkGA1UEBhMCREUxDzANBgNV\nBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRAwDgYDVQQKDAdOZWJ1bG9uMQww\nCgYDVQQLDANDVE8xFTATBgNVBAMMDCouZm9vYmFyLmNvbTBcMA0GCSqGSIb3DQEB\nAQUAA0sAMEgCQQC0FKf07ZWMcABFlZw+GzXK9EiZrlJ1lpnu64RhN99z7MXRr8cF\nnZVgY3jgatuyR5s3WdzUvye2eJ0rNicl2EZJAgMBAAEwDQYJKoZIhvcNAQELBQAD\nQQAw4bteMZAeJWl2wgNLw+wTwAH96E0jyxwreCnT5AxJLmgimyQ0XOF4FsssdRFj\nxD9WA+rktelBodJyPeTDNhIh\n-----END CERTIFICATE-----';
|
||||
var validKey1 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBALQUp/TtlYxwAEWVnD4bNcr0SJmuUnWWme7rhGE333PsxdGvxwWd\nlWBjeOBq27JHmzdZ3NS/J7Z4nSs2JyXYRkkCAwEAAQJALV2eykcoC48TonQEPmkg\nbhaIS57syw67jMLsQImQ02UABKzqHPEKLXPOZhZPS9hsC/hGIehwiYCXMUlrl+WF\nAQIhAOntBI6qaecNjAAVG7UbZclMuHROUONmZUF1KNq6VyV5AiEAxRLkfHWy52CM\njOQrX347edZ30f4QczvugXwsyuU9A1ECIGlGZ8Sk4OBA8n6fAUcyO06qnmCJVlHg\npTUeOvKk5c9RAiBs28+8dCNbrbhVhx/yQr9FwNM0+ttJW/yWJ+pyNQhr0QIgJTT6\nxwCWYOtbioyt7B9l+ENy3AMSO3Uq+xmIKkvItK4=\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
// baz.foobar.com
|
||||
var validCert2 = '-----BEGIN CERTIFICATE-----\nMIIBwjCCAWwCCQDIKtL9RCDCkDANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wHhcN\nMTUxMDI4MTMwNTMzWhcNMTYxMDI3MTMwNTMzWjBoMQswCQYDVQQGEwJERTEPMA0G\nA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05lYnVsb24x\nDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEAw7UWW/VoQePv2l92l3XcntZeyw1nBiHxk1axZwC6auOW\n2/zfA//Tg7fv4q5qKnV1n/71IiMAheeFcpfogY5rTwIDAQABMA0GCSqGSIb3DQEB\nCwUAA0EAtluL6dGNfOdNkzoO/UwzRaIvEm2reuqe+Ik4WR/k+DJ4igrmRCQqXwjW\nJaGYsFWsuk3QLOWQ9YgCKlcIYd+1/A==\n-----END CERTIFICATE-----';
|
||||
var validKey2 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBAMO1Flv1aEHj79pfdpd13J7WXssNZwYh8ZNWsWcAumrjltv83wP/\n04O37+Kuaip1dZ/+9SIjAIXnhXKX6IGOa08CAwEAAQJAUPD3Y2cXDJFaJQXwhWnw\nqhzdLbvITUgCor5rNr+dWhE2MopGPpRHiabA1PeWEPx8CfblyTZGd8KUR/2W1c0r\naQIhAP4ZxB3+uhuzzMfyRrn/khr12pFn/FCIDbwnDbyUxLrTAiEAxSuVOFs+Mupt\nYCz/pPrDCx3eid0wyXRObbkLHOxJiBUCIBTp5fxaBNNW3xnt1OhmIo5Zgd3J4zh1\nmjvMMxM8Y1zFAiAxOP0qsZSoj1+41+MGY9fXaaCJ2F96m3+M4tpEYTTGNQIgdESZ\nz+hzHBeYVbWJpIR8uaNkx7wveUF90FpipXyeTsA=\n-----END RSA PRIVATE KEY-----';
|
||||
|
||||
it('allows both null', function () {
|
||||
expect(certificates.validateCertificate(null, null, 'foobar.com')).to.be(null);
|
||||
});
|
||||
|
||||
it('does not allow only cert', function () {
|
||||
expect(certificates.validateCertificate('cert', null, 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow only key', function () {
|
||||
expect(certificates.validateCertificate(null, 'key', 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow empty string for cert', function () {
|
||||
expect(certificates.validateCertificate('', 'key', 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow empty string for key', function () {
|
||||
expect(certificates.validateCertificate('cert', '', 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow invalid cert', function () {
|
||||
expect(certificates.validateCertificate('someinvalidcert', validKey0, 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow invalid key', function () {
|
||||
expect(certificates.validateCertificate(validCert0, 'invalidkey', 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow cert without matching domain', function () {
|
||||
expect(certificates.validateCertificate(validCert0, validKey0, 'cloudron.io')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain', function () {
|
||||
expect(certificates.validateCertificate(validCert0, validKey0, 'foobar.com')).to.be(null);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain (wildcard)', function () {
|
||||
expect(certificates.validateCertificate(validCert1, validKey1, 'abc.foobar.com')).to.be(null);
|
||||
});
|
||||
|
||||
it('does now allow cert without matching domain (wildcard)', function () {
|
||||
expect(certificates.validateCertificate(validCert1, validKey1, 'foobar.com')).to.be.an(Error);
|
||||
expect(certificates.validateCertificate(validCert1, validKey1, 'bar.abc.foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain (subdomain)', function () {
|
||||
expect(certificates.validateCertificate(validCert2, validKey2, 'baz.foobar.com')).to.be(null);
|
||||
});
|
||||
|
||||
it('does not allow cert without matching domain (subdomain)', function () {
|
||||
expect(certificates.validateCertificate(validCert0, validKey0, 'baz.foobar.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow invalid cert/key tuple', function () {
|
||||
expect(certificates.validateCertificate(validCert0, validKey1, 'foobar.com')).to.be.an(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
set -eu
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
readonly TEST_IMAGE="cloudron/test:10.0.0"
|
||||
|
||||
source ${SOURCE_DIR}/setup/INFRA_VERSION
|
||||
|
||||
@@ -34,28 +35,40 @@ for script in "${scripts[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
if ! docker inspect cloudron/test:8.0.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull cloudron/test:8.0.0 for tests to run"
|
||||
exit 1
|
||||
image_missing=""
|
||||
|
||||
if ! docker inspect "${TEST_IMAGE}" >/dev/null 2>/dev/null; then
|
||||
echo "docker pull ${TEST_IMAGE}"
|
||||
image_missing="true"
|
||||
fi
|
||||
|
||||
if ! docker inspect "${REDIS_IMAGE}" >/dev/null 2>/dev/null; then
|
||||
echo "docker pull ${REDIS_IMAGE} for tests to run"
|
||||
exit 1
|
||||
echo "docker pull ${REDIS_IMAGE}"
|
||||
image_missing="true"
|
||||
fi
|
||||
|
||||
if ! docker inspect "${MYSQL_IMAGE}" >/dev/null 2>/dev/null; then
|
||||
echo "docker pull ${MYSQL_IMAGE} for tests to run"
|
||||
exit 1
|
||||
echo "docker pull ${MYSQL_IMAGE}"
|
||||
image_missing="true"
|
||||
fi
|
||||
|
||||
if ! docker inspect "${POSTGRESQL_IMAGE}" >/dev/null 2>/dev/null; then
|
||||
echo "docker pull ${POSTGRESQL_IMAGE} for tests to run"
|
||||
exit 1
|
||||
echo "docker pull ${POSTGRESQL_IMAGE}"
|
||||
image_missing="true"
|
||||
fi
|
||||
|
||||
if ! docker inspect "${MONGODB_IMAGE}" >/dev/null 2>/dev/null; then
|
||||
echo "docker pull ${MONGODB_IMAGE} for tests to run"
|
||||
echo "docker pull ${MONGODB_IMAGE}"
|
||||
image_missing="true"
|
||||
fi
|
||||
|
||||
if ! docker inspect "${MAIL_IMAGE}" >/dev/null 2>/dev/null; then
|
||||
echo "docker pull ${MAIL_IMAGE}"
|
||||
image_missing="true"
|
||||
fi
|
||||
|
||||
if [[ "${image_missing}" == "true" ]]; then
|
||||
echo "Pull above images before running tests"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -9,17 +9,21 @@
|
||||
var constants = require('../constants.js'),
|
||||
expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
path = require('path');
|
||||
path = require('path'),
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance');
|
||||
|
||||
var config = null;
|
||||
|
||||
describe('config', function () {
|
||||
before(function () {
|
||||
safe.fs.unlinkSync(paths.DNS_IN_SYNC_FILE);
|
||||
delete require.cache[require.resolve('../config.js')];
|
||||
config = require('../config.js');
|
||||
});
|
||||
|
||||
after(function () {
|
||||
safe.fs.unlinkSync(paths.DNS_IN_SYNC_FILE);
|
||||
delete require.cache[require.resolve('../config.js')];
|
||||
});
|
||||
|
||||
@@ -28,6 +32,17 @@ describe('config', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
it('dnsInSync() is unset', function (done) {
|
||||
expect(config.dnsInSync()).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
|
||||
it('dnsInSync() is set', function (done) {
|
||||
config.setDnsInSync();
|
||||
expect(config.dnsInSync()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
|
||||
it('cloudron.conf generated automatically', function (done) {
|
||||
expect(fs.existsSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).to.be.ok();
|
||||
done();
|
||||
|
||||
@@ -11,7 +11,7 @@ var progress = require('../progress.js'),
|
||||
database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
nock = require('nock'),
|
||||
request = require('superagent'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../server.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
@@ -46,7 +46,7 @@ describe('Server', function () {
|
||||
});
|
||||
|
||||
it('is reachable', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
done(err);
|
||||
});
|
||||
@@ -79,32 +79,32 @@ describe('Server', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('random bad requests', function (done) {
|
||||
request.get(SERVER_URL + '/random', function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
it('random bad superagents', function (done) {
|
||||
superagent.get(SERVER_URL + '/random', function (err, res) {
|
||||
expect(err).to.be.ok();
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('version', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.version).to.equal('0.5.0');
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('status route is GET', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/status')
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/status')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/status')
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/status')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -122,18 +122,16 @@ describe('Server', function () {
|
||||
});
|
||||
|
||||
it('config fails due missing token', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/config', function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/config', function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('config fails due wrong token', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/config').query({ access_token: 'somewrongtoken' }).end(function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/config').query({ access_token: 'somewrongtoken' }).end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -150,8 +148,7 @@ describe('Server', function () {
|
||||
});
|
||||
|
||||
it('succeeds with no progress', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.update).to.be(null);
|
||||
expect(result.body.backup).to.be(null);
|
||||
@@ -162,8 +159,7 @@ describe('Server', function () {
|
||||
it('succeeds with update progress', function (done) {
|
||||
progress.set(progress.UPDATE, 13, 'This is some status string');
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.update).to.be.an('object');
|
||||
expect(result.body.update.percent).to.be.a('number');
|
||||
@@ -179,8 +175,7 @@ describe('Server', function () {
|
||||
it('succeeds with no progress after clearing the update', function (done) {
|
||||
progress.clear(progress.UPDATE);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.update).to.be(null);
|
||||
expect(result.body.backup).to.be(null);
|
||||
@@ -211,8 +206,9 @@ describe('Server', function () {
|
||||
});
|
||||
|
||||
it('is not reachable anymore', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/status', function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (error, result) {
|
||||
expect(error).to.not.be(null);
|
||||
expect(!error.response).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -226,15 +222,15 @@ describe('Server', function () {
|
||||
});
|
||||
|
||||
it('responds to OPTIONS', function (done) {
|
||||
request('OPTIONS', SERVER_URL + '/api/v1/cloudron/status')
|
||||
superagent('OPTIONS', SERVER_URL + '/api/v1/cloudron/status')
|
||||
.set('Access-Control-Request-Method', 'GET')
|
||||
.set('Access-Control-Request-Headers', 'accept, origin, x-requested-with')
|
||||
.set('Access-Control-Request-Headers', 'accept, origin, x-superagented-with')
|
||||
.set('Origin', 'http://localhost')
|
||||
.end(function (res) {
|
||||
.end(function (error, res) {
|
||||
expect(res.headers['access-control-allow-methods']).to.be('GET, PUT, DELETE, POST, OPTIONS');
|
||||
expect(res.headers['access-control-allow-credentials']).to.be('true');
|
||||
expect(res.headers['access-control-allow-headers']).to.be('accept, origin, x-requested-with'); // mirrored from request
|
||||
expect(res.headers['access-control-allow-origin']).to.be('http://localhost'); // mirrors from request
|
||||
expect(res.headers['access-control-allow-headers']).to.be('accept, origin, x-superagented-with'); // mirrored from superagent
|
||||
expect(res.headers['access-control-allow-origin']).to.be('http://localhost'); // mirrors from superagent
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,71 +23,122 @@ function cleanup(done) {
|
||||
}
|
||||
|
||||
describe('Settings', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
describe('values', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('can get default timezone', function (done) {
|
||||
settings.getTimeZone(function (error, tz) {
|
||||
expect(error).to.be(null);
|
||||
expect(tz.length).to.not.be(0);
|
||||
done();
|
||||
it('can get default timezone', function (done) {
|
||||
settings.getTimeZone(function (error, tz) {
|
||||
expect(error).to.be(null);
|
||||
expect(tz.length).to.not.be(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can get default autoupdate_pattern', function (done) {
|
||||
settings.getAutoupdatePattern(function (error, pattern) {
|
||||
expect(error).to.be(null);
|
||||
expect(pattern).to.be('00 00 1,3,5,23 * * *');
|
||||
done();
|
||||
it('can get default autoupdate_pattern', function (done) {
|
||||
settings.getAutoupdatePattern(function (error, pattern) {
|
||||
expect(error).to.be(null);
|
||||
expect(pattern).to.be('00 00 1,3,5,23 * * *');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it ('can get default cloudron name', function (done) {
|
||||
settings.getCloudronName(function (error, name) {
|
||||
expect(error).to.be(null);
|
||||
expect(name).to.be('Cloudron');
|
||||
done();
|
||||
it ('can get default cloudron name', function (done) {
|
||||
settings.getCloudronName(function (error, name) {
|
||||
expect(error).to.be(null);
|
||||
expect(name).to.be('Cloudron');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can get default cloudron avatar', function (done) {
|
||||
settings.getCloudronAvatar(function (error, gravatar) {
|
||||
expect(error).to.be(null);
|
||||
expect(gravatar).to.be.a(Buffer);
|
||||
done();
|
||||
it('can get default cloudron avatar', function (done) {
|
||||
settings.getCloudronAvatar(function (error, gravatar) {
|
||||
expect(error).to.be(null);
|
||||
expect(gravatar).to.be.a(Buffer);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can get default developer mode', function (done) {
|
||||
settings.getDeveloperMode(function (error, enabled) {
|
||||
expect(error).to.be(null);
|
||||
expect(enabled).to.equal(false);
|
||||
done();
|
||||
it('can get default developer mode', function (done) {
|
||||
settings.getDeveloperMode(function (error, enabled) {
|
||||
expect(error).to.be(null);
|
||||
expect(enabled).to.equal(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can set developer mode', function (done) {
|
||||
settings.setDeveloperMode(true, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
it('can set developer mode', function (done) {
|
||||
settings.setDeveloperMode(true, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can get developer mode', function (done) {
|
||||
settings.getDeveloperMode(function (error, enabled) {
|
||||
expect(error).to.be(null);
|
||||
expect(enabled).to.equal(true);
|
||||
done();
|
||||
it('can get developer mode', function (done) {
|
||||
settings.getDeveloperMode(function (error, enabled) {
|
||||
expect(error).to.be(null);
|
||||
expect(enabled).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can get all values', function (done) {
|
||||
settings.getAll(function (error, allSettings) {
|
||||
expect(error).to.be(null);
|
||||
expect(allSettings[settings.TIME_ZONE_KEY]).to.be.a('string');
|
||||
expect(allSettings[settings.AUTOUPDATE_PATTERN_KEY]).to.be.a('string');
|
||||
expect(allSettings[settings.CLOUDRON_NAME_KEY]).to.be.a('string');
|
||||
done();
|
||||
it('can set dns config', function (done) {
|
||||
settings.setDnsConfig({ provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey' }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get dns config', function (done) {
|
||||
settings.getDnsConfig(function (error, dnsConfig) {
|
||||
expect(error).to.be(null);
|
||||
expect(dnsConfig.provider).to.be('route53');
|
||||
expect(dnsConfig.accessKeyId).to.be('accessKeyId');
|
||||
expect(dnsConfig.secretAccessKey).to.be('secretAccessKey');
|
||||
expect(dnsConfig.region).to.be('us-east-1');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set tls config', function (done) {
|
||||
settings.setTlsConfig({ provider: 'caas' }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get tls config', function (done) {
|
||||
settings.getTlsConfig(function (error, dnsConfig) {
|
||||
expect(error).to.be(null);
|
||||
expect(dnsConfig.provider).to.be('caas');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set backup config', function (done) {
|
||||
settings.setBackupConfig({ provider: 'caas', token: 'TOKEN' }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get backup config', function (done) {
|
||||
settings.getBackupConfig(function (error, dnsConfig) {
|
||||
expect(error).to.be(null);
|
||||
expect(dnsConfig.provider).to.be('caas');
|
||||
expect(dnsConfig.token).to.be('TOKEN');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get all values', function (done) {
|
||||
settings.getAll(function (error, allSettings) {
|
||||
expect(error).to.be(null);
|
||||
expect(allSettings[settings.TIME_ZONE_KEY]).to.be.a('string');
|
||||
expect(allSettings[settings.AUTOUPDATE_PATTERN_KEY]).to.be.a('string');
|
||||
expect(allSettings[settings.CLOUDRON_NAME_KEY]).to.be.a('string');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ readonly source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)"
|
||||
rm -rf $HOME/.cloudron_test
|
||||
mkdir -p $HOME/.cloudron_test
|
||||
cd $HOME/.cloudron_test
|
||||
mkdir -p data/appdata data/box/appicons data/mail data/nginx/cert data/nginx/applications data/collectd/collectd.conf.d data/addons configs
|
||||
mkdir -p data/appdata data/box/appicons data/mail data/nginx/cert data/nginx/applications data/collectd/collectd.conf.d data/addons configs data/box/certs
|
||||
|
||||
webadmin_scopes="root,profile,users,apps,settings"
|
||||
webadmin_origin="https://${ADMIN_LOCATION}-localhost"
|
||||
|
||||
@@ -53,7 +53,7 @@ function getAppUpdates(callback) {
|
||||
.timeout(10 * 1000)
|
||||
.end(function (error, result) {
|
||||
|
||||
if (error) return callback(error);
|
||||
if (error && !error.response) return callback(error);
|
||||
|
||||
if (result.statusCode !== 200 || !result.body.appVersions) {
|
||||
return callback(new Error(util.format('Error checking app update: %s %s', result.statusCode, result.text)));
|
||||
@@ -88,8 +88,8 @@ function getBoxUpdates(callback) {
|
||||
.get(config.get('boxVersionsUrl'))
|
||||
.timeout(10 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.status !== 200) return callback(new Error(util.format('Bad status: %s %s', result.status, result.text)));
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new Error(util.format('Bad status: %s %s', result.statusCode, result.text)));
|
||||
|
||||
var versions = safe.JSON.parse(result.text);
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = waitForDns;
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
attempt = require('attempt'),
|
||||
debug = require('debug')('box:src/waitfordns'),
|
||||
dns = require('native-dns');
|
||||
|
||||
// the first arg to callback is not an error argument; this is required for async.every
|
||||
function isChangeSynced(domain, ip, nameserver, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof nameserver, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// ns records cannot have cname
|
||||
dns.resolve4(nameserver, function (error, nsIps) {
|
||||
if (error || !nsIps || nsIps.length === 0) return callback(false);
|
||||
|
||||
async.every(nsIps, function (nsIp, iteratorCallback) {
|
||||
var req = dns.Request({
|
||||
question: dns.Question({ name: domain, type: 'A' }),
|
||||
server: { address: nsIp },
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
req.on('timeout', function () { return iteratorCallback(false); });
|
||||
|
||||
req.on('message', function (error, message) {
|
||||
if (error || !message.answer || message.answer.length === 0) return iteratorCallback(false);
|
||||
|
||||
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, message.answer[0], ip);
|
||||
|
||||
if (message.answer[0].address !== ip) return iteratorCallback(false);
|
||||
|
||||
iteratorCallback(true); // done
|
||||
});
|
||||
|
||||
req.send();
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
// check if IP change has propagated to every nameserver
|
||||
function waitForDns(domain, ip, zoneName, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
|
||||
var defaultOptions = {
|
||||
retryInterval: 5000,
|
||||
retries: Infinity
|
||||
};
|
||||
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = defaultOptions;
|
||||
} else {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
}
|
||||
|
||||
debug('waitForDNS: domain %s to be %s in zone %s.', domain, ip, zoneName);
|
||||
|
||||
attempt(function (attempts) {
|
||||
var callback = this; // gross
|
||||
debug('waitForDNS: %s attempt %s.', domain, attempts);
|
||||
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(error || new Error('Unable to get nameservers'));
|
||||
|
||||
async.every(nameservers, isChangeSynced.bind(null, domain, ip), function (synced) {
|
||||
debug('waitForDNS: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
|
||||
|
||||
callback(synced ? null : new Error('ETRYAGAIN'));
|
||||
});
|
||||
});
|
||||
}, { interval: options.retryInterval, retries: options.retries }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('waitForDNS: %s done.', domain);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
@@ -32,7 +32,7 @@ function backupDone(filename, app, appBackupIds, callback) {
|
||||
};
|
||||
|
||||
superagent.post(url).send(data).query({ token: config.token() }).end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new Error(result.text));
|
||||
if (!result.body) return callback(new Error('Unexpected response'));
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
{
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1021 B |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@@ -120,8 +120,8 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand navbar-brand-icon" href="index.html"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></a>
|
||||
<a class="navbar-brand" href="index.html">Cloudron</a>
|
||||
<a class="navbar-brand navbar-brand-icon" href="#/"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></a>
|
||||
<a class="navbar-brand" href="#/">Cloudron</a>
|
||||
</div>
|
||||
<!-- /.navbar-header -->
|
||||
|
||||
@@ -145,9 +145,9 @@
|
||||
<a href="" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><img ng-src="{{user.gravatar}}"/> {{user.username}} <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li>
|
||||
<li ng-show="user.admin && config.isDev"><a href="#/dns"><i class="fa fa-wrench fa-fw"></i> DNS Management</a></li>
|
||||
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
|
||||
<li><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
|
||||
<li ng-show="user.admin && config.isCustomDomain"><a href="#/certs"><i class="fa fa-certificate fa-fw"></i> DNS & Certs</a></li>
|
||||
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li>
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
angular.module('Application').service('Client', ['$http', 'md5', 'Notification', function ($http, md5, Notification) {
|
||||
var client = null;
|
||||
|
||||
// Keep this in sync with docs and docker.js
|
||||
var DEFAULT_MEMORY_LIMIT = 1024 * 1024 * 200;
|
||||
|
||||
function ClientError(statusCode, messageOrObject) {
|
||||
Error.call(this);
|
||||
this.name = this.constructor.name;
|
||||
@@ -58,6 +61,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
this._configListener = [];
|
||||
this._readyListener = [];
|
||||
this._userInfo = {
|
||||
id: null,
|
||||
username: null,
|
||||
email: null,
|
||||
admin: false
|
||||
@@ -75,7 +79,8 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
isCustomDomain: false,
|
||||
developerMode: false,
|
||||
region: null,
|
||||
size: null
|
||||
size: null,
|
||||
memory: 0
|
||||
};
|
||||
this._installedApps = [];
|
||||
this._clientId = '<%= oauth.clientId %>';
|
||||
@@ -118,6 +123,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
|
||||
Client.prototype.setUserInfo = function (userInfo) {
|
||||
// In order to keep the angular bindings alive, set each property individually
|
||||
this._userInfo.id = userInfo.id;
|
||||
this._userInfo.username = userInfo.username;
|
||||
this._userInfo.email = userInfo.email;
|
||||
this._userInfo.admin = !!userInfo.admin;
|
||||
@@ -200,7 +206,17 @@ 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, oauthProxy: config.oauthProxy };
|
||||
var data = {
|
||||
appStoreId: id,
|
||||
manifest: manifest,
|
||||
location: config.location,
|
||||
portBindings: config.portBindings,
|
||||
accessRestriction: config.accessRestriction,
|
||||
oauthProxy: config.oauthProxy,
|
||||
cert: config.cert,
|
||||
key: config.key
|
||||
};
|
||||
|
||||
$http.post(client.apiOrigin + '/api/v1/apps/install', data).success(function (data, status) {
|
||||
if (status !== 202 || typeof data !== 'object') return defaultErrorHandler(callback);
|
||||
|
||||
@@ -234,7 +250,17 @@ 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, oauthProxy: config.oauthProxy };
|
||||
var data = {
|
||||
appId: id,
|
||||
password: password,
|
||||
location: config.location,
|
||||
portBindings: config.portBindings,
|
||||
accessRestriction: config.accessRestriction,
|
||||
oauthProxy: config.oauthProxy,
|
||||
cert: config.cert,
|
||||
key: config.key
|
||||
};
|
||||
|
||||
$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);
|
||||
@@ -288,6 +314,20 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.setDnsConfig = function (dnsConfig, callback) {
|
||||
$http.post(client.apiOrigin + '/api/v1/settings/dns_config', dnsConfig).success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getDnsConfig = function (callback) {
|
||||
$http.get(client.apiOrigin + '/api/v1/settings/dns_config').success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getBackups = function (callback) {
|
||||
$http.get(client.apiOrigin + '/api/v1/backups').success(function (data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
@@ -430,16 +470,14 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
};
|
||||
|
||||
Client.prototype.setCertificate = function (certificateFile, keyFile, callback) {
|
||||
console.log('will set certificate');
|
||||
$http.post(client.apiOrigin + '/api/v1/settings/certificate', { cert: certificateFile, key: keyFile }).success(function(data, status) {
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('certificate', certificateFile);
|
||||
fd.append('key', keyFile);
|
||||
|
||||
$http.post(client.apiOrigin + '/api/v1/cloudron/certificate', fd, {
|
||||
headers: { 'Content-Type': undefined },
|
||||
transformRequest: angular.identity
|
||||
}).success(function(data, status) {
|
||||
Client.prototype.setAdminCertificate = function (certificateFile, keyFile, callback) {
|
||||
$http.post(client.apiOrigin + '/api/v1/settings/admin_certificate', { cert: certificateFile, key: keyFile }).success(function(data, status) {
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
@@ -638,6 +676,14 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.enoughResourcesAvailable = function (app) {
|
||||
var needed = app.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT;
|
||||
var used = this.getInstalledApps().reduce(function (prev, cur) { return prev + (cur.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT); }, 0);
|
||||
var available = (this.getConfig().memory || 0) - used;
|
||||
|
||||
return (available - needed) > 0;
|
||||
};
|
||||
|
||||
client = new Client();
|
||||
return client;
|
||||
}]);
|
||||
|
||||
@@ -25,15 +25,15 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
}).when('/apps', {
|
||||
controller: 'AppsController',
|
||||
templateUrl: 'views/apps.html'
|
||||
}).when('/dns', {
|
||||
controller: 'DnsController',
|
||||
templateUrl: 'views/dns.html'
|
||||
}).when('/account', {
|
||||
controller: 'AccountController',
|
||||
templateUrl: 'views/account.html'
|
||||
}).when('/graphs', {
|
||||
controller: 'GraphsController',
|
||||
templateUrl: 'views/graphs.html'
|
||||
}).when('/certs', {
|
||||
controller: 'CertsController',
|
||||
templateUrl: 'views/certs.html'
|
||||
}).when('/settings', {
|
||||
controller: 'SettingsController',
|
||||
templateUrl: 'views/settings.html'
|
||||
|
||||
@@ -28,8 +28,11 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
controller: 'StepController',
|
||||
templateUrl: 'views/setup/step2.html'
|
||||
}).when('/step3', {
|
||||
controller: 'FinishController',
|
||||
controller: 'StepController',
|
||||
templateUrl: 'views/setup/step3.html'
|
||||
}).when('/step4', {
|
||||
controller: 'FinishController',
|
||||
templateUrl: 'views/setup/step4.html'
|
||||
}).otherwise({ redirectTo: '/'});
|
||||
}]);
|
||||
|
||||
@@ -95,6 +98,7 @@ app.service('Wizard', [ function () {
|
||||
}];
|
||||
this.avatar = {};
|
||||
this.avatarBlob = null;
|
||||
this.dnsConfig = null;
|
||||
}
|
||||
|
||||
Wizard.prototype.setPreviewAvatar = function (avatar) {
|
||||
@@ -146,8 +150,24 @@ app.service('Wizard', [ function () {
|
||||
app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', function ($scope, $route, $location, Wizard) {
|
||||
$scope.wizard = Wizard;
|
||||
|
||||
$scope.next = function (page, bad) {
|
||||
if (!bad) $location.path(page);
|
||||
$scope.next = function (bad) {
|
||||
if (bad) return;
|
||||
|
||||
var current = $location.path();
|
||||
var next = '';
|
||||
|
||||
if (current === '/step1') {
|
||||
next = '/step2';
|
||||
} else if (current === '/step2') {
|
||||
if (Wizard.dnsConfig === null) next = '/step4';
|
||||
else next = '/step3';
|
||||
} else if (current === '/step3') {
|
||||
next = '/step4';
|
||||
} else {
|
||||
next = '/step1';
|
||||
}
|
||||
|
||||
$location.path(next);
|
||||
};
|
||||
|
||||
$scope.focusNext = function (elemId, bad) {
|
||||
@@ -190,11 +210,13 @@ app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', fun
|
||||
image = null;
|
||||
};
|
||||
image.src = $scope.wizard.availableAvatars[randomIndex].data || $scope.wizard.availableAvatars[randomIndex].url;
|
||||
} else if ($route.current.templateUrl === 'views/setup/step3.html' && Wizard.dnsConfig === null) {
|
||||
$location.path('/step4'); // not using custom domain
|
||||
}
|
||||
|
||||
}]);
|
||||
|
||||
app.controller('FinishController', ['$scope', '$location', '$timeout', 'Wizard', 'Client', function ($scope, $location, $timeout, Wizard, Client) {
|
||||
app.controller('FinishController', ['$scope', '$location', 'Wizard', 'Client', function ($scope, $location, Wizard, Client) {
|
||||
$scope.wizard = Wizard;
|
||||
|
||||
Client.createAdmin($scope.wizard.username, $scope.wizard.password, $scope.wizard.email, $scope.setupToken, function (error) {
|
||||
@@ -207,7 +229,16 @@ app.controller('FinishController', ['$scope', '$location', '$timeout', 'Wizard',
|
||||
Client.changeCloudronAvatar($scope.wizard.avatarBlob, function (error) {
|
||||
if (error) return console.error('Unable to set avatar.', error);
|
||||
|
||||
window.location.href = '/';
|
||||
if ($scope.wizard.dnsConfig === null) {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
Client.setDnsConfig($scope.wizard.dnsConfig, function (error) {
|
||||
if (error) return console.error('Unable to set dns config.', error);
|
||||
|
||||
window.location.href = '/';
|
||||
});
|
||||
});
|
||||
});
|
||||
}]);
|
||||
@@ -224,7 +255,13 @@ app.controller('SetupController', ['$scope', '$location', 'Client', 'Wizard', fu
|
||||
if (!search.email) return window.location.href = '/error.html?errorCode=3';
|
||||
Wizard.email = search.email;
|
||||
|
||||
Wizard.hostname = window.location.host.indexOf('my-') === 0 ? window.location.host.slice(3) : window.location.host;
|
||||
if (search.customDomain === 'true') {
|
||||
Wizard.dnsConfig = {
|
||||
provider: 'route53',
|
||||
accessKeyId: null,
|
||||
secretAccessKey: null
|
||||
};
|
||||
}
|
||||
|
||||
Client.isServerFirstTime(function (error, isFirstTime) {
|
||||
if (error) {
|
||||
|
||||
@@ -42,12 +42,35 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="oauthProxy">Website Visibility</label>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="oauthProxy" ng-model="appConfigure.oauthProxy"> Cloudron users only
|
||||
</label>
|
||||
<select class="form-control" id="oauthProxy" ng-model="appConfigure.oauthProxy">
|
||||
<option value="">Visible to all</option>
|
||||
<option value="1">Visible only to Cloudron users</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<label class="control-label" for="appConfigureCertificateInput" ng-show="config.isCustomDomain">Certificate (optional)</label>
|
||||
<div class="has-error text-center" ng-show="appConfigure.error.cert && config.isCustomDomain">{{ appConfigure.error.cert }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.certificate.$dirty && appConfigure.error.cert }" ng-show="config.isCustomDomain">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appConfigureCertificateFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="appConfigure.certificateFileName" id="appConfigureCertificateInput" name="certificate" onclick="getElementById('appConfigureCertificateFileInput').click();" style="cursor: pointer;" ng-required="appConfigure.keyFileName">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('appConfigureCertificateFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.key.$dirty && appConfigure.error.cert }" ng-show="config.isCustomDomain">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appConfigureKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="appConfigure.keyFileName" id="appConfigureKeyInput" name="key" onclick="getElementById('appConfigureKeyFileInput').click();" style="cursor: pointer;" ng-required="appConfigure.certificateFileName">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('appConfigureKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</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/>
|
||||
<br/>
|
||||
@@ -96,6 +119,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal error app -->
|
||||
<div class="modal fade" id="appErrorModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ appError.app.location }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>There was an error installing this app</p>
|
||||
<p>{{appError.app.installationProgress}}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal uninstall app -->
|
||||
<div class="modal fade" id="appUninstallModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -196,7 +237,7 @@
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<div class="col-sm-1 grid-item" ng-repeat="app in installedApps">
|
||||
<div style="background-color: white;" class="highlight grid-item-content">
|
||||
<a ng-href="{{app | applicationLink}}" target="_blank">
|
||||
<a ng-href="{{app | applicationLink}}" ng-click="(app | installError) === true && showError(app)" target="_blank">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* global ISTATES:false */
|
||||
/* global HSTATES:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
@@ -19,7 +20,11 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
portBindings: {},
|
||||
portBindingsEnabled: {},
|
||||
portBindingsInfo: {},
|
||||
oauthProxy: false
|
||||
oauthProxy: '',
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
};
|
||||
|
||||
$scope.appUninstall = {
|
||||
@@ -36,6 +41,10 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
password: ''
|
||||
};
|
||||
|
||||
$scope.appError = {
|
||||
app: {}
|
||||
};
|
||||
|
||||
$scope.appUpdate = {
|
||||
busy: false,
|
||||
error: {},
|
||||
@@ -51,8 +60,13 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.app = {};
|
||||
$scope.appConfigure.location = '';
|
||||
$scope.appConfigure.password = '';
|
||||
$scope.appConfigure.portBindings = {};
|
||||
$scope.appConfigure.oauthProxy = false;
|
||||
$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
|
||||
$scope.appConfigure.oauthProxy = '';
|
||||
$scope.appConfigure.certificateFile = null;
|
||||
$scope.appConfigure.certificateFileName = '';
|
||||
$scope.appConfigure.keyFile = null;
|
||||
$scope.appConfigure.keyFileName = '';
|
||||
|
||||
$scope.appConfigureForm.$setPristine();
|
||||
$scope.appConfigureForm.$setUntouched();
|
||||
@@ -84,15 +98,42 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appRestoreForm.$setUntouched();
|
||||
};
|
||||
|
||||
document.getElementById('appConfigureCertificateFileInput').onchange = function (event) {
|
||||
$scope.$apply(function () {
|
||||
$scope.appConfigure.certificateFile = null;
|
||||
$scope.appConfigure.certificateFileName = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
$scope.appConfigure.certificateFile = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById('appConfigureKeyFileInput').onchange = function (event) {
|
||||
$scope.$apply(function () {
|
||||
$scope.appConfigure.keyFile = null;
|
||||
$scope.appConfigure.keyFileName = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
$scope.appConfigure.keyFile = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showConfigure = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
// fill relevant info from the app
|
||||
$scope.appConfigure.app = app;
|
||||
$scope.appConfigure.location = app.location;
|
||||
$scope.appConfigure.oauthProxy = app.oauthProxy;
|
||||
$scope.appConfigure.oauthProxy = app.oauthProxy ? '1' : '';
|
||||
$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
|
||||
|
||||
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
|
||||
for (var env in $scope.appConfigure.portBindingsInfo) {
|
||||
@@ -125,8 +166,10 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
var data = {
|
||||
location: $scope.appConfigure.location || '',
|
||||
portBindings: finalPortBindings,
|
||||
oauthProxy: $scope.appConfigure.oauthProxy,
|
||||
accessRestriction: $scope.appConfigure.app.accessRestriction
|
||||
oauthProxy: !!$scope.appConfigure.oauthProxy,
|
||||
accessRestriction: $scope.appConfigure.app.accessRestriction,
|
||||
cert: $scope.appConfigure.certificateFile,
|
||||
key: $scope.appConfigure.keyFile,
|
||||
};
|
||||
|
||||
Client.configureApp($scope.appConfigure.app.id, $scope.appConfigure.password, data, function (error) {
|
||||
@@ -141,6 +184,12 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.error.password = 'Wrong password provided.';
|
||||
$scope.appConfigure.password = '';
|
||||
$('#appConfigurePasswordInput').focus();
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
|
||||
$scope.appConfigure.error.cert = error.message;
|
||||
$scope.appConfigure.certificateFileName = '';
|
||||
$scope.appConfigure.certificateFile = null;
|
||||
$scope.appConfigure.keyFileName = '';
|
||||
$scope.appConfigure.keyFile = null;
|
||||
} else {
|
||||
$scope.appConfigure.error.other = error.message;
|
||||
}
|
||||
@@ -157,6 +206,16 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showError = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
$scope.appError.app = app;
|
||||
|
||||
$('#appErrorModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
};
|
||||
|
||||
$scope.showRestore = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="has-error text-center" ng-show="appInstall.error.port">{{ appInstall.error.port }}</div>
|
||||
<div ng-repeat="(env, info) in appInstall.portBindingsInfo">
|
||||
<ng-form name="portInfo_form">
|
||||
@@ -33,13 +34,37 @@
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="appInstall.app.manifest.singleUser">
|
||||
<label class="control-label" for="accessRestriction">User</label>
|
||||
<p>This is a single user application.</p>
|
||||
<select class="form-control" id="accessRestriction" ng-model="appInstall.accessRestriction" ng-required="appInstall.app.manifest.singleUser">
|
||||
<option ng-repeat="user in users" value="{{user.id}}">{{user.username}} - {{user.email}}</option>
|
||||
<select class="form-control" id="accessRestriction" ng-model="appInstall.accessRestriction" ng-options="user as user.username for user in users track by user.id" ng-required="appInstall.app.manifest.singleUser">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<label class="control-label" for="appInstallCertificateInput" ng-show="config.isCustomDomain">Certificate (optional)</label>
|
||||
<div class="has-error text-center" ng-show="appInstall.error.cert && config.isCustomDomain">{{ appInstall.error.cert }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appInstallForm.certificate.$dirty && appInstall.error.cert }" ng-show="config.isCustomDomain">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appInstallCertificateFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="appInstall.certificateFileName" id="appInstallCertificateInput" name="certificate" onclick="getElementById('appInstallCertificateFileInput').click();" style="cursor: pointer;" ng-required="appInstall.keyFileName">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('appInstallCertificateFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appInstallForm.key.$dirty && appInstall.error.cert }" ng-show="config.isCustomDomain">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appInstallKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="appInstall.keyFileName" id="appInstallKeyInput" name="key" onclick="getElementById('appInstallKeyFileInput').click();" style="cursor: pointer;" ng-required="appInstall.certificateFileName">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('appInstallKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="appInstallForm.$invalid || busy"/>
|
||||
</form>
|
||||
</div>
|
||||
@@ -52,11 +77,15 @@
|
||||
<div ng-bind-html="appInstall.app.manifest.description | markdown2html"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="collapseResourceConstraint" data-toggle="false">
|
||||
<h4 class="text-danger">Not enough resources left to install this application.</h4>
|
||||
<p>The Cloudron's resources can be extended with a model upgrade or available resources may be freed up by uninstalling unused applications.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" ng-show="!appInstall.installFormVisible && user.admin" ng-click="showInstallForm()">Install</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.installFormVisible && user.admin" ng-click="doInstall()" ng-disabled="appInstallForm.$invalid || appInstall.busy"><i class="fa fa-spinner fa-pulse" ng-show="appInstall.busy"></i> Install</button>
|
||||
<button type="button" class="btn btn-success" ng-show="!appInstall.installFormVisible && user.admin && !appInstall.resourceConstraintVisible" ng-click="showInstallForm()">Install</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.installFormVisible && user.admin && !appInstall.resourceConstraintVisible" ng-click="doInstall()" ng-disabled="appInstallForm.$invalid || appInstall.busy"><i class="fa fa-spinner fa-pulse" ng-show="appInstall.busy"></i> Install</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,11 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
portBindings: {},
|
||||
accessRestriction: null,
|
||||
oauthProxy: false,
|
||||
mediaLinks: []
|
||||
mediaLinks: [],
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
};
|
||||
|
||||
$scope.appNotFound = {
|
||||
@@ -140,8 +144,15 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$scope.appInstall.accessRestriction = null;
|
||||
$scope.appInstall.oauthProxy = false;
|
||||
$scope.appInstall.installFormVisible = false;
|
||||
$scope.appInstall.resourceConstraintVisible = false;
|
||||
$scope.appInstall.mediaLinks = [];
|
||||
$scope.appInstall.certificateFile = null;
|
||||
$scope.appInstall.certificateFileName = '';
|
||||
$scope.appInstall.keyFile = null;
|
||||
$scope.appInstall.keyFileName = '';
|
||||
|
||||
$('#collapseInstallForm').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('hide');
|
||||
$('#collapseMediaLinksCarousel').collapse('show');
|
||||
|
||||
$scope.appInstallForm.$setPristine();
|
||||
@@ -149,10 +160,44 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
};
|
||||
|
||||
$scope.showInstallForm = function () {
|
||||
$scope.appInstall.installFormVisible = true;
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseInstallForm').collapse('show');
|
||||
$('#appInstallLocationInput').focus();
|
||||
if (Client.enoughResourcesAvailable($scope.appInstall.app)) {
|
||||
$scope.appInstall.installFormVisible = true;
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseInstallForm').collapse('show');
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else {
|
||||
$scope.appInstall.resourceConstraintVisible = true;
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('show');
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('appInstallCertificateFileInput').onchange = function (event) {
|
||||
$scope.$apply(function () {
|
||||
$scope.appInstall.certificateFile = null;
|
||||
$scope.appInstall.certificateFileName = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
$scope.appInstall.certificateFile = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById('appInstallKeyFileInput').onchange = function (event) {
|
||||
$scope.$apply(function () {
|
||||
$scope.appInstall.keyFile = null;
|
||||
$scope.appInstall.keyFileName = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
$scope.appInstall.keyFile = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showInstall = function (app) {
|
||||
@@ -162,14 +207,12 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
angular.copy(app, $scope.appInstall.app);
|
||||
$('#appInstallModal').modal('show');
|
||||
|
||||
console.log(app)
|
||||
|
||||
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
|
||||
$scope.appInstall.location = app.location;
|
||||
$scope.appInstall.portBindingsInfo = $scope.appInstall.app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$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 ? app.accessRestriction.users[0] : null;
|
||||
$scope.appInstall.accessRestriction = app.accessRestriction ? app.accessRestriction.users[0] : $scope.user;
|
||||
$scope.appInstall.oauthProxy = false;
|
||||
|
||||
// set default ports
|
||||
@@ -202,10 +245,19 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
// translate to accessRestriction object
|
||||
var accessRestriction = $scope.appInstall.app.manifest.singleUser ? {
|
||||
users: [ $scope.appInstall.accessRestriction ]
|
||||
users: [ $scope.appInstall.accessRestriction.id ]
|
||||
} : null;
|
||||
|
||||
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, { location: $scope.appInstall.location || '', portBindings: finalPortBindings, accessRestriction: accessRestriction, oauthProxy: $scope.appInstall.oauthProxy }, function (error) {
|
||||
var data = {
|
||||
location: $scope.appInstall.location || '',
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: accessRestriction,
|
||||
oauthProxy: $scope.appInstall.oauthProxy,
|
||||
cert: $scope.appInstall.certificateFile,
|
||||
key: $scope.appInstall.keyFile,
|
||||
};
|
||||
|
||||
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, data, 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;
|
||||
@@ -215,6 +267,12 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else if (error.statusCode === 402) {
|
||||
$scope.appInstall.error.other = 'Unable to purchase this app<br/>Please make sure your payment is setup <a href="' + $scope.config.webServerOrigin + '/console.html#/userprofile" target="_blank">here</a>';
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
|
||||
$scope.appInstall.error.cert = error.message;
|
||||
$scope.appInstall.certificateFileName = '';
|
||||
$scope.appInstall.certificateFile = null;
|
||||
$scope.appInstall.keyFileName = '';
|
||||
$scope.appInstall.keyFile = null;
|
||||
} else {
|
||||
$scope.appInstall.error.other = error.message;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<div style="max-width: 600px; margin: 0 auto;">
|
||||
<div class="text-left">
|
||||
<h1>DNS & Certs</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;">
|
||||
<div class="text-left">
|
||||
<h3>DNS Credentials</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>Currently only Amazon <a href="https://aws.amazon.com/route53/">Route53</a> is supported. Let us know if you require a different DNS provider <a href="#/support">here</a>.</p>
|
||||
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Access Key Id</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.accessKeyId }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Secret Access Key</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;"><i>hidden</i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;"></td>
|
||||
<td class="text-right" style="vertical-align: top;"><span class="text-success" ng-show="dnsCredentials.success"><b>Done</b></span> <button class="btn btn-outline btn-xs btn-primary" ng-show="!dnsCredentials.formVisible" ng-click="showDnsCredentialsForm()">Change</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="collapse" id="collapseDnsCredentialsForm" data-toggle="false">
|
||||
<p>The security credentials have to be valid for full Route53 access.</p>
|
||||
<form name="dnsCredentialsForm" ng-submit="setDnsCredentials()">
|
||||
<fieldset>
|
||||
<div class="has-error text-center" ng-show="dnsCredentials.error">{{ dnsCredentials.error }}</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }">
|
||||
<label class="control-label" for="dnsCredentialsAccessKeyId">Access Key Id</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.accessKeyId" id="dnsCredentialsAccessKeyId" name="accessKeyId" ng-disabled="dnsCredentials.busy" ng-minlength="16" ng-maxlength="32" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }">
|
||||
<label class="control-label" for="dnsCredentialsSecretAccessKey">Secret Access Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" id="dnsCredentialsSecretAccessKey" name="secretAccessKey" ng-disabled="dnsCredentials.busy" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="dnsCredentialsForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="dnsCredentials.busy"></i> Save</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;">
|
||||
<div class="text-left">
|
||||
<h3>SSL Certificates</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="defaultCertForm" ng-submit="setDefaultCert()">
|
||||
<fieldset>
|
||||
<label class="control-label" for="defaultCertInput">Fallback Certificate</label>
|
||||
<p>A wildcard certificate that will be used for apps installed without a specific certificate.</p>
|
||||
<div class="has-error text-center" ng-show="defaultCert.error">{{ defaultCert.error }}</div>
|
||||
<div class="text-success text-center" ng-show="defaultCert.success"><b>Upload successful</b></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.cert.$dirty && defaultCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="defaultCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="defaultCert.certificateFileName" id="defaultCertInput" name="cert" onclick="getElementById('defaultCertFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('defaultCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.key.$dirty && defaultCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="defaultKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="defaultCert.keyFileName" id="defaultKeyInput" name="key" onclick="getElementById('defaultKeyFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('defaultKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="defaultCertForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="defaultCert.busy"></i> Upload</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="adminCertForm" ng-submit="setAdminCert()">
|
||||
<fieldset>
|
||||
<label class="control-label" for="adminCertInput">Settings Certificate</label>
|
||||
<p>This certificate will be used for this Settings application.</p>
|
||||
<div class="has-error text-center" ng-show="adminCert.error">{{ adminCert.error }}</div>
|
||||
<div class="text-success text-center" ng-show="adminCert.success"><b>Upload successful</b></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!adminCert.cert.$dirty && adminCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="adminCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="adminCert.certificateFileName" id="adminCertInput" name="cert" onclick="getElementById('adminCertFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('adminCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!adminCert.key.$dirty && adminCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="adminKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="adminCert.keyFileName" id="adminKeyInput" name="key" onclick="getElementById('adminKeyFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('adminKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="adminCertForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="adminCert.busy"></i> Upload</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,151 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('CertsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin || !Client.getConfig().isCustomDomain) $location.path('/'); });
|
||||
|
||||
$scope.defaultCert = {
|
||||
error: null,
|
||||
success: false,
|
||||
busy: false,
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
};
|
||||
|
||||
$scope.adminCert = {
|
||||
error: null,
|
||||
success: false,
|
||||
busy: false,
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
};
|
||||
|
||||
$scope.dnsCredentials = {
|
||||
error: null,
|
||||
success: false,
|
||||
busy: false,
|
||||
formVisible: false,
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
provider: 'route53'
|
||||
};
|
||||
|
||||
function readFileLocally(obj, file, fileName) {
|
||||
return function (event) {
|
||||
$scope.$apply(function () {
|
||||
obj[file] = null;
|
||||
obj[fileName] = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
obj[file] = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('defaultCertFileInput').onchange = readFileLocally($scope.defaultCert, 'certificateFile', 'certificateFileName');
|
||||
document.getElementById('defaultKeyFileInput').onchange = readFileLocally($scope.defaultCert, 'keyFile', 'keyFileName');
|
||||
document.getElementById('adminCertFileInput').onchange = readFileLocally($scope.adminCert, 'certificateFile', 'certificateFileName');
|
||||
document.getElementById('adminKeyFileInput').onchange = readFileLocally($scope.adminCert, 'keyFile', 'keyFileName');
|
||||
|
||||
$scope.setDefaultCert = function () {
|
||||
$scope.defaultCert.busy = true;
|
||||
$scope.defaultCert.error = null;
|
||||
$scope.defaultCert.success = false;
|
||||
|
||||
Client.setCertificate($scope.defaultCert.certificateFile, $scope.defaultCert.keyFile, function (error) {
|
||||
if (error) {
|
||||
$scope.defaultCert.error = error.message;
|
||||
} else {
|
||||
$scope.defaultCert.success = true;
|
||||
$scope.defaultCert.certificateFileName = '';
|
||||
$scope.defaultCert.keyFileName = '';
|
||||
}
|
||||
|
||||
$scope.defaultCert.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setAdminCert = function () {
|
||||
$scope.adminCert.busy = true;
|
||||
$scope.adminCert.error = null;
|
||||
$scope.adminCert.success = false;
|
||||
|
||||
Client.setAdminCertificate($scope.adminCert.certificateFile, $scope.adminCert.keyFile, function (error) {
|
||||
if (error) {
|
||||
$scope.adminCert.error = error.message;
|
||||
} else {
|
||||
$scope.adminCert.success = true;
|
||||
$scope.adminCert.certificateFileName = '';
|
||||
$scope.adminCert.keyFileName = '';
|
||||
}
|
||||
|
||||
$scope.adminCert.busy = false;
|
||||
|
||||
// attempt to reload to make the browser get the new certs
|
||||
window.location.reload(true);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setDnsCredentials = function () {
|
||||
$scope.dnsCredentials.busy = true;
|
||||
$scope.dnsCredentials.error = null;
|
||||
$scope.dnsCredentials.success = false;
|
||||
|
||||
var data = {
|
||||
provider: $scope.dnsCredentials.provider,
|
||||
accessKeyId: $scope.dnsCredentials.accessKeyId,
|
||||
secretAccessKey: $scope.dnsCredentials.secretAccessKey
|
||||
};
|
||||
|
||||
Client.setDnsConfig(data, function (error) {
|
||||
if (error) {
|
||||
$scope.dnsCredentials.error = error.message;
|
||||
} else {
|
||||
$scope.dnsCredentials.success = true;
|
||||
|
||||
$scope.dnsConfig.accessKeyId = $scope.dnsCredentials.accessKeyId;
|
||||
$scope.dnsConfig.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
|
||||
|
||||
$scope.dnsCredentials.accessKeyId = '';
|
||||
$scope.dnsCredentials.secretAccessKey = '';
|
||||
|
||||
$('#collapseDnsCredentialsForm').collapse('hide');
|
||||
$scope.dnsCredentials.formVisible = false;
|
||||
|
||||
// attempt to reload to make the browser get the new certs
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
$scope.dnsCredentials.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showDnsCredentialsForm = function () {
|
||||
$scope.dnsCredentials.busy = false;
|
||||
$scope.dnsCredentials.success = false;
|
||||
$scope.dnsCredentials.error = null;
|
||||
$scope.dnsCredentials.accessKeyId = '';
|
||||
$scope.dnsCredentials.secretAccessKey = '';
|
||||
$scope.dnsCredentialsForm.$setPristine();
|
||||
$scope.dnsCredentialsForm.$setUntouched();
|
||||
|
||||
$scope.dnsCredentials.formVisible = true;
|
||||
$('#collapseDnsCredentialsForm').collapse('show');
|
||||
$('#dnsCredentialsAccessKeyId').focus();
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.getDnsConfig(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.dnsConfig = result;
|
||||
});
|
||||
});
|
||||
}]);
|
||||
@@ -1,46 +0,0 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h1>DNS Management</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 grid-item">
|
||||
<div class="grid-item-content">
|
||||
<div class="grid-item-top">
|
||||
<big>Certificate</big>
|
||||
</div>
|
||||
<div class="grid-item-bottom text-right">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<input type="file" id="idCertificate" style="display:none"/>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" onclick="getElementById('idCertificate').click();">Certificate</button>
|
||||
</span>
|
||||
<input type="text" class="form-control" ng-model="certificateFileName" onclick="getElementById('idCertificate').click();" style="cursor: pointer;"/>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('idCertificate').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<input type="file" id="idKey" style="display:none"/>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" onclick="getElementById('idKey').click();">Key</button>
|
||||
</span>
|
||||
<input type="text" class="form-control" ng-model="keyFileName" onclick="getElementById('idKey').click();" style="cursor: pointer;"/>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('idKey').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="btn btn-outline btn-success" ng-click="setCertificate()">Upload Certificate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,35 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('DnsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.certificateFile = null;
|
||||
$scope.certificateFileName = '';
|
||||
$scope.keyFile = null;
|
||||
$scope.keyFileName = '';
|
||||
|
||||
document.getElementById('idCertificate').onchange = function (event) {
|
||||
$scope.$apply(function () {
|
||||
$scope.certificateFile = event.target.files[0];
|
||||
$scope.certificateFileName = event.target.files[0].name;
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById('idKey').onchange = function (event) {
|
||||
$scope.$apply(function () {
|
||||
$scope.keyFile = event.target.files[0];
|
||||
$scope.keyFileName = event.target.files[0].name;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setCertificate = function () {
|
||||
if (!$scope.certificateFile) return console.log('Certificate not set');
|
||||
if (!$scope.keyFile) return console.log('Key not set');
|
||||
|
||||
Client.setCertificate($scope.certificateFile, $scope.keyFile, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
window.setTimeout(window.location.reload.bind(window.location, true), 3000);
|
||||
});
|
||||
};
|
||||
}]);
|
||||
@@ -85,6 +85,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
|
||||
<div class="text-left">
|
||||
<h3>About</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin">
|
||||
<div class="row">
|
||||
<div class="col-xs-4" style="min-width: 150px;">
|
||||
<div class="settings-avatar" ng-click="showChangeAvatar()" style="background-image: url('{{avatar.data || avatar.url}}');">
|
||||
<div class="overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-8">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Model</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.size }} - {{ config.region }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Version</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.version }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
|
||||
<div class="text-left">
|
||||
<h3>Backups</h3>
|
||||
@@ -115,34 +143,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
|
||||
<div class="text-left">
|
||||
<h3>About</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin">
|
||||
<div class="row">
|
||||
<div class="col-xs-4" style="min-width: 150px;">
|
||||
<div class="settings-avatar" ng-click="showChangeAvatar()" style="background-image: url('{{avatar.data || avatar.url}}');">
|
||||
<div class="overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-8">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Model</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.size }} - {{ config.region }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Version</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.version }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
|
||||
<div class="text-left">
|
||||
<h3>Developer Mode</h3>
|
||||
|
||||
@@ -5,6 +5,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.dnsConfig = {};
|
||||
|
||||
$scope.lastBackup = null;
|
||||
$scope.backups = [];
|
||||
@@ -262,7 +263,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
Client.onReady(function () {
|
||||
fetchBackups();
|
||||
|
||||
$scope.avatar.url = '//my-' + $scope.config.fqdn + '/api/v1/cloudron/avatar';
|
||||
$scope.avatar.url = ($scope.config.isCustomDomain ? '//my.' : '//my-') + $scope.config.fqdn + '/api/v1/cloudron/avatar';
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
|
||||
@@ -38,6 +38,6 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<a class="btn btn-primary" href="#/step2">Next</a>
|
||||
<button class="btn btn-primary" ng-click="next()">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||