Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70128458b2 | |||
| 900225957e | |||
| fd8f5e3c71 | |||
| 7382ea2b04 | |||
| 09163b8a2b | |||
| 953398c427 | |||
| 9f7406c235 | |||
| 2e427aa60e | |||
| ab80cc9ea1 | |||
| 321f11c644 | |||
| 47f85434db | |||
| 7717c7b1cd | |||
| 7618aa786c | |||
| f752cb368c | |||
| ca500e2165 | |||
| 371f81b980 | |||
| c68cca9a54 | |||
| 9194be06c3 | |||
| 9eb58cdfe5 | |||
| 99be89012d | |||
| 541fabcb2e | |||
| 915e04eb08 | |||
| 48896d4e50 | |||
| 29682c0944 | |||
| 346b1cb91c | |||
| e552821c01 | |||
| bac3ba101e | |||
| 87c46fe3ea | |||
| f9763b1ad3 | |||
| f1e6116b83 | |||
| 273948c3c7 | |||
| 9c073e7bee | |||
| 8b3edf6efc | |||
| 07e649a2d3 | |||
| 8c63b6716d | |||
| 6fd314fe82 | |||
| 0c7eaf09a9 | |||
| d0988e2d61 | |||
| 4bedbd7167 | |||
| 7ca7901a73 | |||
| d28dfdbd03 | |||
| c85ca3c6e2 | |||
| da934d26af | |||
| f7cc49c5f4 | |||
| 27e263e7fb | |||
| 052050f48b | |||
| 81e29c7c2b | |||
| c3fbead658 | |||
| 36f5b6d678 | |||
| a45b1449de | |||
| a1020ec6b8 | |||
| d384284ec8 | |||
| bd29447a7f | |||
| aa5952fe0b | |||
| 39dc5da05a | |||
| d0e07d995a | |||
| 94408c1c3d | |||
| 66f032a7ee | |||
| 4356df3676 |
@@ -1273,3 +1273,28 @@
|
||||
* Enhance user creation API to take a password
|
||||
* Relax restriction on mailbox names now that it is decoupled from user management
|
||||
* Fix issue where mail container incorrectly advertised CRAM-MD5 support
|
||||
|
||||
[2.3.0]
|
||||
* Add Name.com DNS provider
|
||||
* Fix issue where account setup page was crashing
|
||||
* Add advanced DNS configuration UI
|
||||
* Preserve addon/database configuration across app updates and restores
|
||||
* ManageSieve port now offers STARTTLS
|
||||
|
||||
[2.3.1]
|
||||
* Add Name.com DNS provider
|
||||
* Fix issue where account setup page was crashing
|
||||
* Add advanced DNS configuration UI
|
||||
* Preserve addon/database configuration across app updates and restores
|
||||
* ManageSieve port now offers STARTTLS
|
||||
* Allow mailbox name to be set for apps
|
||||
* Rework the Email server UI
|
||||
* Add the ability to manually trigger a backup of an application
|
||||
* Enable/disable mail from validation within UI
|
||||
* Allow setting app visibility for non-SSO apps
|
||||
* Add Clone UI
|
||||
|
||||
[2.3.2]
|
||||
* Fix issue where multi-db apps were not provisioned correctly
|
||||
* Improve setup, restore views to have field labels
|
||||
|
||||
|
||||
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
box
|
||||
Copyright (C) 2016,2017 Cloudron UG
|
||||
Copyright (C) 2016,2017,2018 Cloudron UG
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
function get_status() {
|
||||
key="$1"
|
||||
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
|
||||
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
|
||||
echo "${currentValue}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
function wait_for_status() {
|
||||
key="$1"
|
||||
expectedValue="$2"
|
||||
|
||||
echo "wait_for_status: $key to be $expectedValue"
|
||||
while true; do
|
||||
if currentValue=$(get_status "${key}"); then
|
||||
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
|
||||
if [[ "${currentValue}" == $expectedValue ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
}
|
||||
|
||||
domain=""
|
||||
domainProvider=""
|
||||
domainConfigJson="{}"
|
||||
domainTlsProvider="letsencrypt-prod"
|
||||
adminUsername="superadmin"
|
||||
adminPassword="Secret123#"
|
||||
adminEmail="admin@server.local"
|
||||
appstoreUserId=""
|
||||
appstoreToken=""
|
||||
backupDir="/var/backups"
|
||||
|
||||
args=$(getopt -o "" -l "domain:,domain-provider:,domain-tls-provider:,admin-username:,admin-password:,admin-email:,appstore-user:,appstore-token:,backup-dir:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2;;
|
||||
--domain-provider) domainProvider="$2"; shift 2;;
|
||||
--domain-tls-provider) domainTlsProvider="$2"; shift 2;;
|
||||
--admin-username) adminUsername="$2"; shift 2;;
|
||||
--admin-password) adminPassword="$2"; shift 2;;
|
||||
--admin-email) adminEmail="$2"; shift 2;;
|
||||
--appstore-user) appstoreUser="$2"; shift 2;;
|
||||
--appstore-token) appstoreToken="$2"; shift 2;;
|
||||
--backup-dir) backupDir="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=> Waiting for cloudron to be ready"
|
||||
wait_for_status "version" '*'
|
||||
|
||||
if [[ $(get_status "webadminStatus") != *'"tls": true'* ]]; then
|
||||
echo "=> Domain setup"
|
||||
dnsSetupData=$(printf '{ "domain": "%s", "adminFqdn": "%s", "provider": "%s", "config": %s, "tlsConfig": { "provider": "%s" } }' "${domain}" "my.${domain}" "${domainProvider}" "$domainConfigJson" "${domainTlsProvider}")
|
||||
|
||||
if ! $curl -X POST -H "Content-Type: application/json" -d "${dnsSetupData}" http://localhost:3000/api/v1/cloudron/dns_setup; then
|
||||
echo "DNS Setup Failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "webadminStatus" '*"tls": true*'
|
||||
else
|
||||
echo "=> Skipping Domain setup"
|
||||
fi
|
||||
|
||||
activationData=$(printf '{"username": "%s", "password":"%s", "email": "%s" }' "${adminUsername}" "${adminPassword}" "${adminEmail}")
|
||||
if [[ $(get_status "activated") == "false" ]]; then
|
||||
echo "=> Activating"
|
||||
|
||||
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/cloudron/activate); then
|
||||
echo "Failed to activate with ${activationData}: ${activationResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "activated" "true"
|
||||
else
|
||||
echo "=> Skipping Activation"
|
||||
fi
|
||||
|
||||
echo "=> Getting token"
|
||||
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/developer/login); then
|
||||
echo "Failed to login with ${activationData}: ${activationResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
accessToken=$(echo "${activationResult}" | python3 -c 'import sys, json; print(json.load(sys.stdin)[sys.argv[1]])' "accessToken")
|
||||
|
||||
echo "=> Setting up App Store account with accessToken ${accessToken}"
|
||||
appstoreData=$(printf '{"userId":"%s", "token":"%s" }' "${appstoreUser}" "${appstoreToken}")
|
||||
|
||||
if ! appstoreResult=$($curl -X POST -H "Content-Type: application/json" -d "${appstoreData}" "http://localhost:3000/api/v1/settings/appstore_config?access_token=${accessToken}"); then
|
||||
echo "Failed to setup Appstore account with ${appstoreData}: ${appstoreResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Setting up Backup Directory with accessToken ${accessToken}"
|
||||
backupData=$(printf '{"provider":"filesystem", "key":"", "backupFolder":"%s", "retentionSecs": 864000, "format": "tgz"}' "${backupDir}")
|
||||
|
||||
chown -R yellowtent:yellowtent "${backupDir}"
|
||||
|
||||
if ! backupResult=$($curl -X POST -H "Content-Type: application/json" -d "${backupData}" "http://localhost:3000/api/v1/settings/backup_config?access_token=${accessToken}"); then
|
||||
echo "Failed to setup backup configuration with ${backupDir}: ${backupResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Done!"
|
||||
|
||||
+187
-105
@@ -22,11 +22,12 @@ var accesscontrol = require('./accesscontrol.js'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
ClientsError = clients.ClientsError,
|
||||
crypto = require('crypto'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:addons'),
|
||||
docker = require('./docker.js'),
|
||||
dockerConnection = docker.connection,
|
||||
fs = require('fs'),
|
||||
generatePassword = require('password-generator'),
|
||||
hat = require('hat'),
|
||||
infra = require('./infra_version.js'),
|
||||
mail = require('./mail.js'),
|
||||
@@ -364,23 +365,28 @@ function setupSendMail(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up SendMail');
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
appdb.getAddonConfigByName(app.id, 'sendmail', 'MAIL_SMTP_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
|
||||
var password = error ? hat(4 * 128) : existingPassword;
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -401,23 +407,28 @@ function setupRecvMail(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up recvmail');
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
appdb.getAddonConfigByName(app.id, 'recvmail', 'MAIL_IMAP_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
|
||||
var password = error ? hat(4 * 128) : existingPassword;
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_IMAP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_IMAP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -431,6 +442,14 @@ function teardownRecvMail(app, options, callback) {
|
||||
appdb.unsetAddonConfig(app.id, 'recvmail', callback);
|
||||
}
|
||||
|
||||
function mysqlDatabaseName(appId) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
|
||||
var md5sum = crypto.createHash('md5'); // get rid of "-"
|
||||
md5sum.update(appId);
|
||||
return md5sum.digest('hex').substring(0, 16); // max length of mysql usernames is 16
|
||||
}
|
||||
|
||||
function setupMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
@@ -438,16 +457,36 @@ function setupMySql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up mysql');
|
||||
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', app.id ];
|
||||
appdb.getAddonConfigByName(app.id, 'mysql', 'MYSQL_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error, stdout) {
|
||||
if (error) return callback(error);
|
||||
const dbname = mysqlDatabaseName(app.id);
|
||||
const password = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
|
||||
|
||||
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', dbname, password ];
|
||||
|
||||
debugApp(app, 'Setting mysql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mysql', env, callback);
|
||||
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
{ name: 'MYSQL_USERNAME', value: dbname },
|
||||
{ name: 'MYSQL_PASSWORD', value: password },
|
||||
{ name: 'MYSQL_HOST', value: 'mysql' },
|
||||
{ name: 'MYSQL_PORT', value: '3306' }
|
||||
];
|
||||
|
||||
if (options.multipleDatabases) {
|
||||
env = env.concat({ name: 'MYSQL_DATABASE_PREFIX', value: `${dbname}_` });
|
||||
} else {
|
||||
env = env.concat(
|
||||
{ name: 'MYSQL_URL', value: `mysql://${dbname}:${password}@mysql/${dbname}` },
|
||||
{ name: 'MYSQL_DATABASE', value: dbname }
|
||||
);
|
||||
}
|
||||
|
||||
debugApp(app, 'Setting mysql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mysql', env, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -456,7 +495,8 @@ function teardownMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', app.id ];
|
||||
const dbname = mysqlDatabaseName(app.id);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', dbname ];
|
||||
|
||||
debugApp(app, 'Tearing down mysql');
|
||||
|
||||
@@ -479,7 +519,8 @@ function backupMySql(app, options, callback) {
|
||||
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ];
|
||||
const dbname = mysqlDatabaseName(app.id);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', dbname ];
|
||||
|
||||
docker.execContainer('mysql', cmd, { stdout: output }, callback);
|
||||
}
|
||||
@@ -499,7 +540,8 @@ function restoreMySql(app, options, callback) {
|
||||
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
|
||||
input.on('error', callback);
|
||||
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ];
|
||||
const dbname = mysqlDatabaseName(app.id);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', dbname ];
|
||||
docker.execContainer('mysql', cmd, { stdin: input }, callback);
|
||||
});
|
||||
}
|
||||
@@ -511,16 +553,29 @@ function setupPostgreSql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up postgresql');
|
||||
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'add', app.id ];
|
||||
appdb.getAddonConfigByName(app.id, 'postgresql', 'POSTGRESQL_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error, stdout) {
|
||||
if (error) return callback(error);
|
||||
const password = error ? hat(4 * 128) : existingPassword;
|
||||
const appId = app.id.replace(/-/g, '');
|
||||
|
||||
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'add', appId, password ];
|
||||
|
||||
debugApp(app, 'Setting postgresql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
|
||||
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
{ name: 'POSTGRESQL_URL', value: `postgres://user${appId}:${password}@postgresql/db${appId}` },
|
||||
{ name: 'POSTGRESQL_USERNAME', value: `user${appId}` },
|
||||
{ name: 'POSTGRESQL_PASSWORD', value: password },
|
||||
{ name: 'POSTGRESQL_HOST', value: 'postgresql' },
|
||||
{ name: 'POSTGRESQL_PORT', value: '5432' },
|
||||
{ name: 'POSTGRESQL_DATABASE', value: `db${appId}` }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting postgresql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -529,7 +584,9 @@ function teardownPostgreSql(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'remove', app.id ];
|
||||
const appId = app.id.replace(/-/g, '');
|
||||
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'remove', appId ];
|
||||
|
||||
debugApp(app, 'Tearing down postgresql');
|
||||
|
||||
@@ -552,7 +609,8 @@ function backupPostgreSql(app, options, callback) {
|
||||
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'backup', app.id ];
|
||||
const appId = app.id.replace(/-/g, '');
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'backup', appId ];
|
||||
|
||||
docker.execContainer('postgresql', cmd, { stdout: output }, callback);
|
||||
}
|
||||
@@ -572,7 +630,8 @@ function restorePostgreSql(app, options, callback) {
|
||||
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump'));
|
||||
input.on('error', callback);
|
||||
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'restore', app.id ];
|
||||
const appId = app.id.replace(/-/g, '');
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'restore', appId ];
|
||||
|
||||
docker.execContainer('postgresql', cmd, { stdin: input }, callback);
|
||||
});
|
||||
@@ -585,16 +644,30 @@ function setupMongoDb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up mongodb');
|
||||
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'add', app.id ];
|
||||
appdb.getAddonConfigByName(app.id, 'mongodb', 'MONGODB_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error, stdout) {
|
||||
if (error) return callback(error);
|
||||
const password = error ? hat(4 * 128) : existingPassword;
|
||||
|
||||
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
|
||||
const dbname = app.id;
|
||||
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'add', dbname, password ];
|
||||
|
||||
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
{ name: 'MONGODB_URL', value : `mongodb://${dbname}:${password}@mongodb/${dbname}` },
|
||||
{ name: 'MONGODB_USERNAME', value : dbname },
|
||||
{ name: 'MONGODB_PASSWORD', value: password },
|
||||
{ name: 'MONGODB_HOST', value : 'mongodb' },
|
||||
{ name: 'MONGODB_PORT', value : '27017' },
|
||||
{ name: 'MONGODB_DATABASE', value : dbname }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -603,7 +676,8 @@ function teardownMongoDb(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'remove', app.id ];
|
||||
const dbname = app.id;
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'remove', dbname ];
|
||||
|
||||
debugApp(app, 'Tearing down mongodb');
|
||||
|
||||
@@ -626,7 +700,8 @@ function backupMongoDb(app, options, callback) {
|
||||
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'backup', app.id ];
|
||||
const dbname = app.id;
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'backup', dbname ];
|
||||
|
||||
docker.execContainer('mongodb', cmd, { stdout: output }, callback);
|
||||
}
|
||||
@@ -646,7 +721,9 @@ function restoreMongoDb(app, options, callback) {
|
||||
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump'));
|
||||
input.on('error', callback);
|
||||
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'restore', app.id ];
|
||||
const dbname = app.id;
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'restore', dbname ];
|
||||
|
||||
docker.execContainer('mongodb', cmd, { stdin: input }, callback);
|
||||
});
|
||||
}
|
||||
@@ -657,58 +734,63 @@ function setupRedis(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var redisPassword = generatePassword(128, false /* memorable */, /[\w\d_]/); // ensure no / in password for being sed friendly (and be uri friendly)
|
||||
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
var redisDataDir = path.join(paths.APPS_DATA_DIR, app.id + '/redis');
|
||||
appdb.getAddonConfigByName(app.id, 'redis', 'REDIS_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
if (!safe.fs.writeFileSync(redisVarsFile, 'REDIS_PASSWORD=' + redisPassword)) {
|
||||
return callback(new Error('Error writing redis config'));
|
||||
}
|
||||
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
|
||||
|
||||
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
|
||||
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
var redisDataDir = path.join(paths.APPS_DATA_DIR, app.id + '/redis');
|
||||
|
||||
// Compute redis memory limit based on app's memory limit (this is arbitrary)
|
||||
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
|
||||
if (!safe.fs.writeFileSync(redisVarsFile, 'REDIS_PASSWORD=' + redisPassword)) {
|
||||
return callback(new Error('Error writing redis config'));
|
||||
}
|
||||
|
||||
if (memoryLimit === -1) { // unrestricted (debug mode)
|
||||
memoryLimit = 0;
|
||||
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
|
||||
memoryLimit = 150 * 1024 * 1024; // 150m
|
||||
} else {
|
||||
memoryLimit = 600 * 1024 * 1024; // 600m
|
||||
}
|
||||
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
|
||||
|
||||
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
|
||||
const label = app.fqdn;
|
||||
// note that we do not add appId label because this interferes with the stop/start app logic
|
||||
const cmd = `docker run --restart=always -d --name=${redisName} \
|
||||
--label=location=${label} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
-m ${memoryLimit/2} \
|
||||
--memory-swap ${memoryLimit} \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
|
||||
-v ${redisDataDir}:/var/lib/redis:rw \
|
||||
--read-only -v /tmp -v /run ${tag}`;
|
||||
// Compute redis memory limit based on app's memory limit (this is arbitrary)
|
||||
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
|
||||
|
||||
var env = [
|
||||
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: 'REDIS_PASSWORD', value: redisPassword },
|
||||
{ name: 'REDIS_HOST', value: redisName },
|
||||
{ name: 'REDIS_PORT', value: '6379' }
|
||||
];
|
||||
if (memoryLimit === -1) { // unrestricted (debug mode)
|
||||
memoryLimit = 0;
|
||||
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
|
||||
memoryLimit = 150 * 1024 * 1024; // 150m
|
||||
} else {
|
||||
memoryLimit = 600 * 1024 * 1024; // 600m
|
||||
}
|
||||
|
||||
async.series([
|
||||
// stop so that redis can flush itself with SIGTERM
|
||||
shell.execSync.bind(null, 'stopRedis', `docker stop --time=10 ${redisName} 2>/dev/null || true`),
|
||||
shell.execSync.bind(null, 'stopRedis', `docker rm --volumes ${redisName} 2>/dev/null || true`),
|
||||
shell.execSync.bind(null, 'startRedis', cmd),
|
||||
appdb.setAddonConfig.bind(null, app.id, 'redis', env)
|
||||
], function (error) {
|
||||
if (error) debug('Error setting up redis: ', error);
|
||||
callback(error);
|
||||
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
|
||||
const label = app.fqdn;
|
||||
// note that we do not add appId label because this interferes with the stop/start app logic
|
||||
const cmd = `docker run --restart=always -d --name=${redisName} \
|
||||
--label=location=${label} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
-m ${memoryLimit/2} \
|
||||
--memory-swap ${memoryLimit} \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
|
||||
-v ${redisDataDir}:/var/lib/redis:rw \
|
||||
--read-only -v /tmp -v /run ${tag}`;
|
||||
|
||||
var env = [
|
||||
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: 'REDIS_PASSWORD', value: redisPassword },
|
||||
{ name: 'REDIS_HOST', value: redisName },
|
||||
{ name: 'REDIS_PORT', value: '6379' }
|
||||
];
|
||||
|
||||
async.series([
|
||||
// stop so that redis can flush itself with SIGTERM
|
||||
shell.execSync.bind(null, 'stopRedis', `docker stop --time=10 ${redisName} 2>/dev/null || true`),
|
||||
shell.execSync.bind(null, 'stopRedis', `docker rm --volumes ${redisName} 2>/dev/null || true`),
|
||||
shell.execSync.bind(null, 'startRedis', cmd),
|
||||
appdb.setAddonConfig.bind(null, app.id, 'redis', env)
|
||||
], function (error) {
|
||||
if (error) debug('Error setting up redis: ', error);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+33
-9
@@ -66,6 +66,7 @@ var addons = require('./addons.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
groups = require('./groups.js'),
|
||||
mail = require('./mail.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
os = require('os'),
|
||||
@@ -314,7 +315,6 @@ function getAppConfig(app) {
|
||||
manifest: app.manifest,
|
||||
location: app.location,
|
||||
domain: app.domain,
|
||||
fqdn: app.fqdn,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
memoryLimit: app.memoryLimit,
|
||||
@@ -325,8 +325,9 @@ function getAppConfig(app) {
|
||||
}
|
||||
|
||||
function removeInternalAppFields(app) {
|
||||
return _.pick(app, 'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
|
||||
'location', 'domain', 'fqdn',
|
||||
return _.pick(app,
|
||||
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
|
||||
'location', 'domain', 'fqdn', 'mailboxName',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
|
||||
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime');
|
||||
}
|
||||
@@ -376,7 +377,13 @@ function get(appId, callback) {
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
|
||||
callback(null, app);
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -398,7 +405,13 @@ function getByIpAddress(ip, callback) {
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
|
||||
callback(null, app);
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -417,7 +430,13 @@ function getAll(callback) {
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
|
||||
iteratorDone();
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
iteratorDone(null, app);
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -652,6 +671,11 @@ function configure(appId, data, auditSource, callback) {
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
if ('mailboxName' in data) {
|
||||
error = mail.validateName(data.mailboxName);
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
@@ -681,8 +705,8 @@ function configure(appId, data, auditSource, callback) {
|
||||
|
||||
debug('Will configure app with id:%s values:%j', appId, values);
|
||||
|
||||
var oldName = (app.location ? app.location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
var newName = (location ? location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
var oldName = app.mailboxName;
|
||||
var newName = data.mailboxName || app.mailboxName;
|
||||
mailboxdb.updateName(oldName, values.oldConfig.domain, newName, domain, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
|
||||
@@ -1203,7 +1227,7 @@ function restoreInstalledApps(callback) {
|
||||
|
||||
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
|
||||
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: getAppConfig(app) }, function (error) {
|
||||
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
|
||||
|
||||
iteratorDone(); // always succeed
|
||||
|
||||
+38
-11
@@ -5,6 +5,7 @@ exports = module.exports = {
|
||||
unpurchase: unpurchase,
|
||||
|
||||
getSubscription: getSubscription,
|
||||
isFreePlan: isFreePlan,
|
||||
|
||||
sendAliveStatus: sendAliveStatus,
|
||||
|
||||
@@ -18,7 +19,8 @@ exports = module.exports = {
|
||||
AppstoreError: AppstoreError
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
var appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
@@ -89,6 +91,10 @@ function getSubscription(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function isFreePlan(subscription) {
|
||||
return !subscription || subscription.plan.id === 'free';
|
||||
}
|
||||
|
||||
function purchase(appId, appstoreId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appstoreId, 'string');
|
||||
@@ -96,20 +102,41 @@ function purchase(appId, appstoreId, callback) {
|
||||
|
||||
if (appstoreId === '') return callback(null);
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
function doThePurchase() {
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
var data = { appstoreId: appstoreId };
|
||||
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getSubscription(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
var data = { appstoreId: appstoreId };
|
||||
// only check for app install count if on the free plan
|
||||
if (result.id !== 'free') return doThePurchase();
|
||||
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
appdb.getAppStoreIds(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
var count = result.filter(function (a) { return !!a.appStoreId; }).length;
|
||||
|
||||
// we only allow max of 2 app installations without a subscription
|
||||
// WARNING install and clone in apps.js will first add the db record and then call purchase() so we test for more than 2 here
|
||||
if (count > 2) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, 'Too many apps installed'));
|
||||
|
||||
doThePurchase();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+9
-3
@@ -341,7 +341,7 @@ function waitForDnsPropagation(app, callback) {
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 120 }, callback);
|
||||
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 240 }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -374,8 +374,14 @@ function install(app, callback) {
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
// oldConfig can be null during upgrades
|
||||
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : app.manifest.addons),
|
||||
function teardownAddons(next) {
|
||||
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
|
||||
var addonsToRemove = !isRestoring
|
||||
? app.manifest.addons
|
||||
: _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
|
||||
|
||||
addons.teardownAddons(app, addonsToRemove, next);
|
||||
},
|
||||
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
|
||||
|
||||
// for restore case
|
||||
|
||||
+2
-4
@@ -28,11 +28,9 @@ function maybeSend(callback) {
|
||||
var pendingAppUpdates = updateInfo.apps || {};
|
||||
pendingAppUpdates = Object.keys(pendingAppUpdates).map(function (key) { return pendingAppUpdates[key]; });
|
||||
|
||||
appstore.getSubscription(function (error, result) {
|
||||
appstore.getSubscription(function (error, subscription) {
|
||||
if (error) debug('Error getting subscription:', error);
|
||||
|
||||
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
|
||||
|
||||
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -46,7 +44,7 @@ function maybeSend(callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var info = {
|
||||
hasSubscription: hasSubscription,
|
||||
hasSubscription: appstore.isFreePlan(subscription),
|
||||
|
||||
pendingAppUpdates: pendingAppUpdates,
|
||||
pendingBoxUpdate: updateInfo.box || null,
|
||||
|
||||
+3
-3
@@ -174,14 +174,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !resolvedNS) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
getZoneByName(credentials, zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var definedNS = zone.metadata.nameServers.sort().map(function(r) { return r.replace(/\.$/, ''); });
|
||||
if (!_.isEqual(definedNS, resolvedNS.sort())) {
|
||||
debug('verifyDnsConfig: %j and %j do not match', resolvedNS, definedNS);
|
||||
if (!_.isEqual(definedNS, nameservers.sort())) {
|
||||
debug('verifyDnsConfig: %j and %j do not match', nameservers, definedNS);
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -57,7 +57,8 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
|
||||
// Very basic check if the nameservers can be fetched
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to get nameservers'));
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
callback(null, { wildcard: !!dnsConfig.wildcard });
|
||||
});
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:dns/namecom'),
|
||||
dns = require('../native-dns.js'),
|
||||
safe = require('safetydance'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
superagent = require('superagent');
|
||||
|
||||
const NAMECOM_API = 'https://api.name.com/v4';
|
||||
|
||||
function formatError(response) {
|
||||
return `Name.com DNS error [${response.statusCode}] ${response.text}`;
|
||||
}
|
||||
|
||||
function addRecord(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(Array.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`add: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
var data = {
|
||||
host: subdomain,
|
||||
type: type,
|
||||
ttl: 300 // 300 is the lowest
|
||||
};
|
||||
|
||||
if (type === 'MX') {
|
||||
data.priority = parseInt(values[0].split(' ')[0], 10);
|
||||
data.answer = values[0].split(' ')[1];
|
||||
} else {
|
||||
data.answer = values[0];
|
||||
}
|
||||
|
||||
superagent.post(`${NAMECOM_API}/domains/${zoneName}/records`)
|
||||
.auth(dnsConfig.username, dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.send(data)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
|
||||
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null, 'unused-id');
|
||||
});
|
||||
}
|
||||
|
||||
function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof recordId, 'number');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`update:${recordId} on ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
var data = {
|
||||
host: subdomain,
|
||||
type: type,
|
||||
ttl: 300 // 300 is the lowest
|
||||
};
|
||||
|
||||
if (type === 'MX') {
|
||||
data.priority = parseInt(values[0].split(' ')[0], 10);
|
||||
data.answer = values[0].split(' ')[1];
|
||||
} else {
|
||||
data.answer = values[0];
|
||||
}
|
||||
|
||||
superagent.put(`${NAMECOM_API}/domains/${zoneName}/records/${recordId}`)
|
||||
.auth(dnsConfig.username, dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.send(data)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
|
||||
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null, 'unused-id');
|
||||
});
|
||||
}
|
||||
|
||||
function getInternal(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');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`getInternal: ${subdomain} in zone ${zoneName} of type ${type}`);
|
||||
|
||||
superagent.get(`${NAMECOM_API}/domains/${zoneName}/records`)
|
||||
.auth(dnsConfig.username, dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
|
||||
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
// name.com does not return the correct content-type
|
||||
result.body = safe.JSON.parse(result.text);
|
||||
if (!result.body.records) result.body.records = [];
|
||||
|
||||
result.body.records.forEach(function (r) {
|
||||
// name.com api simply strips empty properties
|
||||
r.host = r.host || '@';
|
||||
});
|
||||
|
||||
var results = result.body.records.filter(function (r) {
|
||||
return (r.host === subdomain && r.type === type);
|
||||
});
|
||||
|
||||
debug('getInternal: %j', results);
|
||||
|
||||
return callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(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(Array.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.length === 0) return addRecord(dnsConfig, zoneName, subdomain, type, values, callback);
|
||||
|
||||
return updateRecord(dnsConfig, zoneName, result[0].id, 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');
|
||||
|
||||
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var tmp = result.map(function (record) { return record.answer; });
|
||||
|
||||
debug('get: %j', tmp);
|
||||
|
||||
return callback(null, tmp);
|
||||
});
|
||||
}
|
||||
|
||||
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(Array.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.length === 0) return callback();
|
||||
|
||||
superagent.del(`${NAMECOM_API}/domains/${zoneName}/records/${result[0].id}`)
|
||||
.auth(dnsConfig.username, dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
|
||||
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var credentials = {
|
||||
username: dnsConfig.username,
|
||||
token: dnsConfig.token
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
if (!nameservers.every(function (n) { return n.toLowerCase().indexOf('.name.com') !== -1; })) {
|
||||
debug('verifyDnsConfig: %j does not contain Name.com NS', nameservers);
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Name.com'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -262,7 +262,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
+14
-4
@@ -68,7 +68,7 @@ DomainsError.STILL_BUSY = 'Still busy';
|
||||
DomainsError.IN_USE = 'In Use';
|
||||
DomainsError.INTERNAL_ERROR = 'Internal error';
|
||||
DomainsError.ACCESS_DENIED = 'Access denied';
|
||||
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, noop, manual or caas';
|
||||
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, manual or caas';
|
||||
|
||||
// choose which subdomain backend we use for test purpose we use route53
|
||||
function api(provider) {
|
||||
@@ -82,6 +82,7 @@ function api(provider) {
|
||||
case 'digitalocean': return require('./dns/digitalocean.js');
|
||||
case 'gandi': return require('./dns/gandi.js');
|
||||
case 'godaddy': return require('./dns/godaddy.js');
|
||||
case 'namecom': return require('./dns/namecom.js');
|
||||
case 'noop': return require('./dns/noop.js');
|
||||
case 'manual': return require('./dns/manual.js');
|
||||
default: return null;
|
||||
@@ -113,9 +114,11 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!tld.isValid(domain)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
|
||||
if (domain.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
|
||||
|
||||
if (zoneName) {
|
||||
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
|
||||
if (zoneName.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
|
||||
} else {
|
||||
zoneName = tld.getDomain(domain) || domain;
|
||||
}
|
||||
@@ -188,8 +191,9 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function update(domain, provider, config, fallbackCertificate, tlsConfig, callback) {
|
||||
function update(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof fallbackCertificate, 'object');
|
||||
@@ -200,6 +204,12 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (zoneName) {
|
||||
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
|
||||
} else {
|
||||
zoneName = result.zoneName;
|
||||
}
|
||||
|
||||
if (fallbackCertificate) {
|
||||
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
|
||||
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
@@ -212,7 +222,7 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
verifyDnsConfig(config, domain, result.zoneName, provider, ip, function (error, result) {
|
||||
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
|
||||
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record:' + error.message));
|
||||
@@ -220,7 +230,7 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
domaindb.update(domain, { provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
|
||||
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
exports = module.exports = {
|
||||
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
|
||||
// a minor version makes all apps re-configure themselves
|
||||
'version': '48.9.0',
|
||||
'version': '48.10.0',
|
||||
|
||||
'baseImages': [ 'cloudron/base:0.10.0' ],
|
||||
|
||||
// Note that if any of the databases include an upgrade, bump the infra version above
|
||||
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
|
||||
'images': {
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.0.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.0.0' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.0.1' },
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.1.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.1.0' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.1.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.2.3' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.3.0' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
|
||||
}
|
||||
};
|
||||
|
||||
+28
-20
@@ -12,6 +12,8 @@ exports = module.exports = {
|
||||
|
||||
addDnsRecords: addDnsRecords,
|
||||
|
||||
validateName: validateName,
|
||||
|
||||
setMailFromValidation: setMailFromValidation,
|
||||
setCatchAllAddress: setCatchAllAddress,
|
||||
setMailRelay: setMailRelay,
|
||||
@@ -45,8 +47,9 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
appstore = require('./appstore.js'),
|
||||
AppstoreError = appstore.AppstoreError,
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:mail'),
|
||||
dns = require('./native-dns.js'),
|
||||
@@ -92,10 +95,12 @@ function MailError(reason, errorOrMessage) {
|
||||
}
|
||||
util.inherits(MailError, Error);
|
||||
MailError.INTERNAL_ERROR = 'Internal Error';
|
||||
MailError.EXTERNAL_ERROR = 'External Error';
|
||||
MailError.BAD_FIELD = 'Bad Field';
|
||||
MailError.ALREADY_EXISTS = 'Already Exists';
|
||||
MailError.NOT_FOUND = 'Not Found';
|
||||
MailError.IN_USE = 'In Use';
|
||||
MailError.BILLING_REQUIRED = 'Billing Required';
|
||||
|
||||
function validateName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
@@ -357,11 +362,6 @@ const RBL_LIST = [
|
||||
'dns': 'spam.dnsbl.sorbs.net',
|
||||
'site': 'http://sorbs.net'
|
||||
},
|
||||
{
|
||||
'name': 'Spam Cannibal',
|
||||
'dns': 'bl.spamcannibal.org',
|
||||
'site': 'http://www.spamcannibal.org/cannibal.cgi'
|
||||
},
|
||||
{
|
||||
'name': 'SpamCop',
|
||||
'dns': 'bl.spamcop.net',
|
||||
@@ -823,25 +823,33 @@ function setMailEnabled(domain, enabled, callback) {
|
||||
assert.strictEqual(typeof enabled, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
maildb.update(domain, { enabled: enabled }, function (error) {
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
appstore.getSubscription(function (error, result) {
|
||||
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new MailError(MailError.BILLING_REQUIRED));
|
||||
if (error) return callback(new MailError(MailError.EXTERNAL_ERROR, error));
|
||||
|
||||
restartMail(NOOP_CALLBACK);
|
||||
// we only allow enabling email on a paid plan
|
||||
if (enabled && appstore.isFreePlan(result)) return callback(new MailError(MailError.BILLING_REQUIRED));
|
||||
|
||||
if (!enabled || process.env.BOX_ENV === 'test') return callback(null);
|
||||
maildb.update(domain, { enabled: enabled }, function (error) {
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
// Add MX and DMARC record. Note that DMARC policy depends on DKIM signing and thus works
|
||||
// only if we use our internal mail server.
|
||||
var records = [
|
||||
{ subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] },
|
||||
{ subdomain: '', type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] }
|
||||
];
|
||||
restartMail(NOOP_CALLBACK);
|
||||
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
domains.upsertDnsRecords(record.subdomain, domain, record.type, record.values, iteratorCallback);
|
||||
}, NOOP_CALLBACK);
|
||||
if (!enabled || process.env.BOX_ENV === 'test') return callback(null);
|
||||
|
||||
callback(null);
|
||||
// Add MX and DMARC record. Note that DMARC policy depends on DKIM signing and thus works
|
||||
// only if we use our internal mail server.
|
||||
var records = [
|
||||
{ subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] },
|
||||
{ subdomain: '', type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] }
|
||||
];
|
||||
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
domains.upsertDnsRecords(record.subdomain, domain, record.type, record.values, iteratorCallback);
|
||||
}, NOOP_CALLBACK);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+13
-14
@@ -60,6 +60,19 @@ function splatchError(error) {
|
||||
return util.inspect(result, { depth: null, showHidden: true });
|
||||
}
|
||||
|
||||
function getAdminEmails(callback) {
|
||||
users.getAllAdmins(function (error, admins) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
|
||||
|
||||
var adminEmails = [ ];
|
||||
admins.forEach(function (admin) { adminEmails.push(admin.email); });
|
||||
|
||||
callback(null, adminEmails);
|
||||
});
|
||||
}
|
||||
|
||||
// This will collect the most common details required for notification emails
|
||||
function getMailConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -157,20 +170,6 @@ function render(templateFile, params) {
|
||||
return content;
|
||||
}
|
||||
|
||||
function getAdminEmails(callback) {
|
||||
users.getAllAdmins(function (error, admins) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
|
||||
|
||||
var adminEmails = [ ];
|
||||
adminEmails.push(admins[0].fallbackEmail);
|
||||
admins.forEach(function (admin) { adminEmails.push(admin.email); });
|
||||
|
||||
callback(null, adminEmails);
|
||||
});
|
||||
}
|
||||
|
||||
function mailUserEventToAdmins(user, event) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof event, 'string');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">© 2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted">© 2016-18 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
|
||||
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
+30
-3
@@ -22,6 +22,7 @@ var apps = require('./apps.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
util = require('util'),
|
||||
@@ -47,8 +48,15 @@ function start(callback) {
|
||||
// short-circuit for the restart case
|
||||
if (_.isEqual(infra, existingInfra)) {
|
||||
debug('platform is uptodate at version %s', infra.version);
|
||||
emitPlatformReady();
|
||||
return callback();
|
||||
|
||||
updateAddons(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
emitPlatformReady();
|
||||
|
||||
callback();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
debug('Updating infrastructure from %s to %s', existingInfra.version, infra.version);
|
||||
@@ -61,7 +69,8 @@ function start(callback) {
|
||||
startAddons.bind(null, existingInfra),
|
||||
removeOldImages,
|
||||
startApps.bind(null, existingInfra),
|
||||
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra))
|
||||
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4)),
|
||||
updateAddons
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -80,6 +89,24 @@ function stop(callback) {
|
||||
taskmanager.pauseTasks(callback);
|
||||
}
|
||||
|
||||
function updateAddons(callback) {
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
for (var containerName of [ 'mysql', 'postgresql', 'mail', 'mongodb' ]) {
|
||||
const containerConfig = platformConfig[containerName];
|
||||
if (!containerConfig) continue;
|
||||
|
||||
if (!containerConfig.memory || !containerConfig.memorySwap) continue;
|
||||
|
||||
const cmd = `docker update --memory ${containerConfig.memory} --memory-swap ${containerConfig.memorySwap} ${containerName}`;
|
||||
shell.execSync(`update${containerName}`, cmd);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function emitPlatformReady() {
|
||||
// give some time for the platform to "settle". For example, mysql might still be initing the
|
||||
// database dir and we cannot call service scripts until that's done.
|
||||
|
||||
@@ -386,6 +386,7 @@ function renewAll(auditSource, callback) {
|
||||
|
||||
async.eachSeries(allApps, function (app, iteratorCallback) {
|
||||
ensureCertificate(app, auditSource, function (error, bundle) {
|
||||
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
|
||||
if (bundle.reason !== 'new-le' && bundle.reason !== 'fallback') return iteratorCallback();
|
||||
|
||||
// reconfigure for the case where we got a renewed cert after fallback
|
||||
|
||||
@@ -170,6 +170,8 @@ function configureApp(req, res, next) {
|
||||
|
||||
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
|
||||
|
||||
if ('mailboxName' in data && typeof data.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
|
||||
|
||||
debug('Configuring app id:%s data:%j', req.params.id, data);
|
||||
|
||||
apps.configure(req.params.id, data, auditSource(req), function (error) {
|
||||
|
||||
@@ -17,7 +17,7 @@ function login(req, res, next) {
|
||||
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
|
||||
if (user.twoFactorAuthenticationEnabled) {
|
||||
if (!user.ghost && user.twoFactorAuthenticationEnabled) {
|
||||
if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided'));
|
||||
|
||||
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken });
|
||||
|
||||
@@ -67,6 +67,7 @@ function update(req, res, next) {
|
||||
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be an object'));
|
||||
if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
|
||||
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
|
||||
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
|
||||
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.cert || typeof req.body.fallbackCertificate.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
||||
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.key || typeof req.body.fallbackCertificate.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
||||
@@ -76,7 +77,7 @@ function update(req, res, next) {
|
||||
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
|
||||
req.clearTimeout();
|
||||
|
||||
domains.update(req.params.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
|
||||
domains.update(req.params.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
|
||||
|
||||
@@ -182,6 +182,7 @@ function setMailEnabled(req, res, next) {
|
||||
mail.setMailEnabled(req.params.domain, !!req.body.enabled, function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === MailError.BILLING_REQUIRED) return next(new HttpError(402, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202));
|
||||
|
||||
@@ -286,7 +286,7 @@ function login(req, res) {
|
||||
passport.authenticate('local', {
|
||||
failureRedirect: '/api/v1/session/login?' + failureQuery
|
||||
})(req, res, function () {
|
||||
if (req.user.twoFactorAuthenticationEnabled) {
|
||||
if (!req.user.ghost && req.user.twoFactorAuthenticationEnabled) {
|
||||
if (!req.body.totpToken) {
|
||||
let failureQuery = querystring.stringify({ error: 'A 2FA token is required', returnTo: returnTo });
|
||||
return res.redirect('/api/v1/session/login?' + failureQuery);
|
||||
@@ -397,7 +397,7 @@ function accountSetup(req, res, next) {
|
||||
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
|
||||
res.redirect(config.adminOrigin());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,6 +78,7 @@ function dnsSetup(req, res, next) {
|
||||
setup.dnsSetup(req.body.adminFqdn.toLowerCase(), req.body.domain.toLowerCase(), req.body.zoneName || '', req.body.provider, req.body.config, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
|
||||
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
|
||||
@@ -372,7 +372,7 @@ describe('Clients', function () {
|
||||
setup,
|
||||
|
||||
function (callback) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
@@ -536,7 +536,7 @@ describe('Clients', function () {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
// further calls with this token should not work
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
@@ -192,7 +192,7 @@ describe('Developer API', function () {
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
|
||||
secret = result.body.secret;
|
||||
callback(error);
|
||||
});
|
||||
@@ -203,7 +203,7 @@ describe('Developer API', function () {
|
||||
encoding: 'base32'
|
||||
});
|
||||
|
||||
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
@@ -213,7 +213,7 @@ describe('Developer API', function () {
|
||||
after(function (done) {
|
||||
async.series([
|
||||
function (callback) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error, result) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error, result) {
|
||||
callback(error);
|
||||
});
|
||||
},
|
||||
@@ -285,14 +285,14 @@ describe('Developer API', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('fails with non sdk token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password').query({ access_token: token_normal }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password').query({ access_token: token_normal }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password').query({ access_token: token_sdk }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password').query({ access_token: token_sdk }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ function setup(done) {
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
|
||||
@@ -601,7 +601,7 @@ describe('OAuth2', function () {
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
|
||||
secret = result.body.secret;
|
||||
callback(error);
|
||||
});
|
||||
@@ -612,7 +612,7 @@ describe('OAuth2', function () {
|
||||
encoding: 'base32'
|
||||
});
|
||||
|
||||
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
@@ -865,7 +865,7 @@ describe('OAuth2', function () {
|
||||
expect(foo.token_type).to.eql('Bearer');
|
||||
|
||||
// Ensure the token is also usable
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile?access_token=' + foo.access_token, function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile?access_token=' + foo.access_token, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.status).to.eql(200);
|
||||
expect(result.body.username).to.equal(USER_0.username.toLowerCase());
|
||||
@@ -1252,7 +1252,7 @@ describe('OAuth2', function () {
|
||||
expect(body.token_type).to.eql('Bearer');
|
||||
|
||||
// Ensure the token is also usable
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile?access_token=' + body.access_token, function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile?access_token=' + body.access_token, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.status).to.eql(200);
|
||||
expect(result.body.username).to.equal(USER_0.username.toLowerCase());
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('Profile API', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile/').end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
@@ -81,7 +81,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('fails with empty token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile/').query({ access_token: '' }).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: '' }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
@@ -89,7 +89,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('fails with invalid token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile/').query({ access_token: 'some token' }).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: 'some token' }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
@@ -97,7 +97,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile/').query({ access_token: token_0 }).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: token_0 }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
|
||||
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
|
||||
@@ -120,7 +120,7 @@ describe('Profile API', function () {
|
||||
tokendb.add(token, user_0.id, null, expires, accesscontrol.SCOPE_ANY, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile').query({ access_token: token }).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
@@ -129,14 +129,14 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('fails with invalid token in auth header', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile').set('Authorization', 'Bearer ' + 'x' + token_0).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile').set('Authorization', 'Bearer ' + 'x' + token_0).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with token in auth header', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile').set('Authorization', 'Bearer ' + token_0).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile').set('Authorization', 'Bearer ' + token_0).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
|
||||
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
|
||||
@@ -154,7 +154,7 @@ describe('Profile API', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('change email fails due to missing token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile')
|
||||
.send({ email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
@@ -163,7 +163,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('change email fails due to invalid email', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ email: 'foo@bar' })
|
||||
.end(function (error, result) {
|
||||
@@ -173,7 +173,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('change user succeeds without email nor displayName', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.send({})
|
||||
.end(function (error, result) {
|
||||
@@ -183,13 +183,13 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('change email succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ email: EMAIL_0_NEW, fallbackEmail: EMAIL_0_NEW_FALLBACK })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -205,13 +205,13 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('change displayName succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ displayName: DISPLAY_NAME_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -231,7 +231,7 @@ describe('Profile API', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('fails due to missing current password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ newPassword: 'some wrong password' })
|
||||
.end(function (err, res) {
|
||||
@@ -241,7 +241,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('fails due to missing new password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
@@ -251,7 +251,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('fails due to wrong password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
@@ -261,7 +261,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('fails due to invalid password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: PASSWORD, newPassword: 'five' })
|
||||
.end(function (err, res) {
|
||||
@@ -271,7 +271,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('Users API', function () {
|
||||
// stash for later use
|
||||
token = res.body.token;
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile').query({ access_token: token }).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
|
||||
expect(error).to.eql(null);
|
||||
expect(result.status).to.equal(200);
|
||||
|
||||
@@ -703,7 +703,7 @@ describe('Users API', function () {
|
||||
});
|
||||
|
||||
it('can get profile of user with pre-set password', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
|
||||
+6
-7
@@ -131,13 +131,12 @@ function initializeExpressSync() {
|
||||
// working off the user behind the provided token
|
||||
router.get ('/api/v1/user/apps', profileScope, routes.apps.getAllByUser);
|
||||
router.get ('/api/v1/user/cloudron_config', profileScope, routes.user.getCloudronConfig);
|
||||
router.get ('/api/v1/profile', profileScope, routes.profile.get); // duplicate route for compatibility
|
||||
router.get ('/api/v1/user/profile', profileScope, routes.profile.get);
|
||||
router.post('/api/v1/user/profile', profileScope, routes.profile.update);
|
||||
router.post('/api/v1/user/profile/password', profileScope, routes.users.verifyPassword, routes.profile.changePassword);
|
||||
router.post('/api/v1/user/profile/twofactorauthentication', profileScope, routes.profile.setTwoFactorAuthenticationSecret);
|
||||
router.post('/api/v1/user/profile/twofactorauthentication/enable', profileScope, routes.profile.enableTwoFactorAuthentication);
|
||||
router.post('/api/v1/user/profile/twofactorauthentication/disable', profileScope, routes.users.verifyPassword, routes.profile.disableTwoFactorAuthentication);
|
||||
router.get ('/api/v1/profile', profileScope, routes.profile.get);
|
||||
router.post('/api/v1/profile', profileScope, routes.profile.update);
|
||||
router.post('/api/v1/profile/password', profileScope, routes.users.verifyPassword, routes.profile.changePassword);
|
||||
router.post('/api/v1/profile/twofactorauthentication', profileScope, routes.profile.setTwoFactorAuthenticationSecret);
|
||||
router.post('/api/v1/profile/twofactorauthentication/enable', profileScope, routes.profile.enableTwoFactorAuthentication);
|
||||
router.post('/api/v1/profile/twofactorauthentication/disable', profileScope, routes.users.verifyPassword, routes.profile.disableTwoFactorAuthentication);
|
||||
|
||||
// user routes
|
||||
router.get ('/api/v1/users', usersScope, routes.users.list);
|
||||
|
||||
+29
-1
@@ -35,6 +35,9 @@ exports = module.exports = {
|
||||
getEmailDigest: getEmailDigest,
|
||||
setEmailDigest: setEmailDigest,
|
||||
|
||||
getPlatformConfig: getPlatformConfig,
|
||||
setPlatformConfig: setPlatformConfig,
|
||||
|
||||
getAll: getAll,
|
||||
|
||||
// booleans. if you add an entry here, be sure to fix getAll
|
||||
@@ -46,6 +49,7 @@ exports = module.exports = {
|
||||
UPDATE_CONFIG_KEY: 'update_config',
|
||||
APPSTORE_CONFIG_KEY: 'appstore_config',
|
||||
CAAS_CONFIG_KEY: 'caas_config',
|
||||
PLATFORM_CONFIG_KEY: 'platform_config',
|
||||
|
||||
// strings
|
||||
APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern',
|
||||
@@ -88,6 +92,7 @@ var gDefaults = (function () {
|
||||
result[exports.APPSTORE_CONFIG_KEY] = {};
|
||||
result[exports.CAAS_CONFIG_KEY] = {};
|
||||
result[exports.EMAIL_DIGEST] = true;
|
||||
result[exports.PLATFORM_CONFIG_KEY] = {};
|
||||
|
||||
return result;
|
||||
})();
|
||||
@@ -371,6 +376,29 @@ function getAppstoreConfig(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getPlatformConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.PLATFORM_CONFIG_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.PLATFORM_CONFIG_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, JSON.parse(value));
|
||||
});
|
||||
}
|
||||
|
||||
function setPlatformConfig(platformConfig, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.set(exports.PLATFORM_CONFIG_KEY, JSON.stringify(platformConfig), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.PLATFORM_CONFIG_KEY, platformConfig);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function setAppstoreConfig(appstoreConfig, callback) {
|
||||
assert.strictEqual(typeof appstoreConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -443,7 +471,7 @@ function getAll(callback) {
|
||||
result[exports.DYNAMIC_DNS_KEY] = !!result[exports.DYNAMIC_DNS_KEY];
|
||||
|
||||
// convert JSON objects
|
||||
[exports.BACKUP_CONFIG_KEY, exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY ].forEach(function (key) {
|
||||
[exports.BACKUP_CONFIG_KEY, exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY ].forEach(function (key) {
|
||||
result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]);
|
||||
});
|
||||
|
||||
|
||||
+4
-5
@@ -11,8 +11,7 @@ exports = module.exports = {
|
||||
SetupError: SetupError
|
||||
};
|
||||
|
||||
var accesscontrol = require('./accesscontrol.js'),
|
||||
assert = require('assert'),
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
@@ -33,7 +32,6 @@ var accesscontrol = require('./accesscontrol.js'),
|
||||
semver = require('semver'),
|
||||
settingsdb = require('./settingsdb.js'),
|
||||
settings = require('./settings.js'),
|
||||
SettingsError = settings.SettingsError,
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
@@ -176,6 +174,7 @@ function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, c
|
||||
|
||||
function done(error) {
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
config.setAdminDomain(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
|
||||
@@ -192,9 +191,9 @@ function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, c
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, result) {
|
||||
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
if (result) return callback(new SettingsError(SettingsError.ALREADY_EXISTS, 'domain already exists'));
|
||||
if (result) return callback(new SetupError(SetupError.BAD_STATE, 'Domain already exists'));
|
||||
|
||||
async.series([
|
||||
domains.add.bind(null, domain, zoneName, provider, dnsConfig, null /* cert */, tlsConfig),
|
||||
|
||||
@@ -10,7 +10,6 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
child_process = require('child_process'),
|
||||
debug = require('debug')('box:shell'),
|
||||
fs = require('fs'),
|
||||
once = require('once'),
|
||||
util = require('util');
|
||||
|
||||
|
||||
+14
-10
@@ -102,7 +102,9 @@ describe('Apps', function () {
|
||||
},
|
||||
portBindings: { PORT: 5678 },
|
||||
accessRestriction: null,
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
robotsTxt: null,
|
||||
sso: false
|
||||
};
|
||||
|
||||
var APP_1 = {
|
||||
@@ -114,7 +116,7 @@ describe('Apps', function () {
|
||||
version: '0.1', dockerImage: 'docker/app1', healthCheckPath: '/', httpPort: 80, title: 'app1',
|
||||
tcpPorts: {}
|
||||
},
|
||||
portBindings: null,
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'someuser' ], groups: [ GROUP_0.id ] },
|
||||
memoryLimit: 0
|
||||
};
|
||||
@@ -128,9 +130,11 @@ describe('Apps', function () {
|
||||
version: '0.1', dockerImage: 'docker/app2', healthCheckPath: '/', httpPort: 80, title: 'app2',
|
||||
tcpPorts: {}
|
||||
},
|
||||
portBindings: null,
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'someuser', USER_0.id ], groups: [ GROUP_1.id ] },
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
robotsTxt: null,
|
||||
sso: false
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
@@ -409,12 +413,12 @@ describe('Apps', function () {
|
||||
apps.restoreInstalledApps(function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
apps.getAll(function (error, apps) {
|
||||
expect(apps[0].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
|
||||
expect(apps[0].oldConfig).to.be(null);
|
||||
expect(apps[1].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
|
||||
expect(apps[2].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
|
||||
expect(apps[2].oldConfig).to.be(null);
|
||||
apps.getAll(function (error, result) {
|
||||
expect(result[0].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
|
||||
expect(result[0].oldConfig).to.eql(apps.getAppConfig(APP_0));
|
||||
expect(result[1].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
|
||||
expect(result[2].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
|
||||
expect(result[2].oldConfig).to.eql(apps.getAppConfig(APP_2));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -125,13 +125,18 @@ describe('Appstore', function () {
|
||||
});
|
||||
|
||||
it('can purchase an app', function (done) {
|
||||
var scope = nock('http://localhost:6060')
|
||||
var scope1 = nock('http://localhost:6060')
|
||||
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(201, {});
|
||||
|
||||
var scope2 = nock('http://localhost:6060')
|
||||
.get(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/subscription?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(200, { subscription: { id: 'basic' }});
|
||||
|
||||
appstore.purchase(APP_ID, APPSTORE_APP_ID, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -95,6 +95,8 @@ const TEST_DOMAIN = {
|
||||
};
|
||||
|
||||
describe('database', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
before(function (done) {
|
||||
config._reset();
|
||||
config.setFqdn(TEST_DOMAIN.domain);
|
||||
|
||||
@@ -123,7 +123,7 @@ describe('digest', function () {
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
checkMails(1, `${USER_0.fallbackEmail}, ${USER_0.email}`, done);
|
||||
checkMails(1, `${USER_0.email}`, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('digest', function () {
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
checkMails(1, `${USER_0.fallbackEmail}, ${USER_0.email}`, done);
|
||||
checkMails(1, `${USER_0.email}`, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,7 +146,7 @@ describe('digest', function () {
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
checkMails(1, `${USER_0.fallbackEmail}, ${USER_0.email}`, done);
|
||||
checkMails(1, `${USER_0.email}`, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+100
-6
@@ -51,7 +51,7 @@ describe('dns provider', function () {
|
||||
DOMAIN_0.provider = 'noop';
|
||||
DOMAIN_0.config = {};
|
||||
|
||||
domains.update(DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
domains.update(DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
});
|
||||
|
||||
it('upsert succeeds', function (done) {
|
||||
@@ -92,7 +92,7 @@ describe('dns provider', function () {
|
||||
token: TOKEN
|
||||
};
|
||||
|
||||
domains.update(DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
domains.update(DOMAIN_0.domain, '', DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
});
|
||||
|
||||
it('upsert non-existing record succeeds', function (done) {
|
||||
@@ -352,7 +352,7 @@ describe('dns provider', function () {
|
||||
apiSecret: SECRET
|
||||
};
|
||||
|
||||
domains.update(DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
domains.update(DOMAIN_0.domain, '', DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
});
|
||||
|
||||
it('upsert record succeeds', function (done) {
|
||||
@@ -439,7 +439,7 @@ describe('dns provider', function () {
|
||||
token: TOKEN
|
||||
};
|
||||
|
||||
domains.update(DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
domains.update(DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
});
|
||||
|
||||
it('upsert record succeeds', function (done) {
|
||||
@@ -503,6 +503,100 @@ describe('dns provider', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('name.com', function () {
|
||||
const TOKEN = 'sometoken';
|
||||
const NAMECOM_API = 'https://api.name.com/v4';
|
||||
|
||||
before(function (done) {
|
||||
DOMAIN_0.provider = 'namecom';
|
||||
DOMAIN_0.config = {
|
||||
token: TOKEN
|
||||
};
|
||||
|
||||
domains.update(DOMAIN_0.domain, '', DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
});
|
||||
|
||||
it('upsert record succeeds', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var DOMAIN_RECORD_0 = {
|
||||
host: 'test',
|
||||
type: 'A',
|
||||
answer: '1.2.3.4',
|
||||
ttl: 300
|
||||
};
|
||||
|
||||
var req1 = nock(NAMECOM_API)
|
||||
.get(`/domains/${DOMAIN_0.zoneName}/records`)
|
||||
.reply(200, { records: [] });
|
||||
|
||||
var req2 = nock(NAMECOM_API)
|
||||
.post(`/domains/${DOMAIN_0.zoneName}/records`, DOMAIN_RECORD_0)
|
||||
.reply(200, {});
|
||||
|
||||
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) {
|
||||
expect(error).to.eql(null);
|
||||
expect(req1.isDone()).to.be.ok();
|
||||
expect(req2.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var DOMAIN_RECORD_0 = {
|
||||
host: 'test',
|
||||
type: 'A',
|
||||
answer: '1.2.3.4',
|
||||
ttl: 300
|
||||
};
|
||||
|
||||
var req1 = nock(NAMECOM_API)
|
||||
.get(`/domains/${DOMAIN_0.zoneName}/records`)
|
||||
.reply(200, { records: [ DOMAIN_RECORD_0 ] });
|
||||
|
||||
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
|
||||
expect(error).to.eql(null);
|
||||
expect(result).to.be.an(Array);
|
||||
expect(result.length).to.eql(1);
|
||||
expect(result[0]).to.eql(DOMAIN_RECORD_0.answer);
|
||||
expect(req1.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('del succeeds', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var DOMAIN_RECORD_0 = {
|
||||
id: 'someid',
|
||||
host: 'test',
|
||||
type: 'A',
|
||||
answer: '1.2.3.4',
|
||||
ttl: 300
|
||||
};
|
||||
|
||||
var req1 = nock(NAMECOM_API)
|
||||
.get(`/domains/${DOMAIN_0.zoneName}/records`)
|
||||
.reply(200, { records: [ DOMAIN_RECORD_0 ] });
|
||||
|
||||
var req2 = nock(NAMECOM_API)
|
||||
.delete(`/domains/${DOMAIN_0.zoneName}/records/${DOMAIN_RECORD_0.id}`)
|
||||
.reply(200, {});
|
||||
|
||||
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
|
||||
expect(error).to.eql(null);
|
||||
expect(req1.isDone()).to.be.ok();
|
||||
expect(req2.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('route53', function () {
|
||||
// do not clear this with [] but .length = 0 so we don't loose the reference in mockery
|
||||
var awsAnswerQueue = [];
|
||||
@@ -581,7 +675,7 @@ describe('dns provider', function () {
|
||||
AWS._originalRoute53 = AWS.Route53;
|
||||
AWS.Route53 = Route53Mock;
|
||||
|
||||
domains.update(DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
domains.update(DOMAIN_0.domain, '', DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
@@ -737,7 +831,7 @@ describe('dns provider', function () {
|
||||
_OriginalGCDNS = GCDNS.prototype.getZones;
|
||||
GCDNS.prototype.getZones = mockery(zoneQueue);
|
||||
|
||||
domains.update(DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
domains.update(DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
|
||||
+44
-2
@@ -2,6 +2,7 @@
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
/* global beforeEach:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
@@ -11,7 +12,10 @@ var async = require('async'),
|
||||
domains = require('../domains.js'),
|
||||
expect = require('expect.js'),
|
||||
mail = require('../mail.js'),
|
||||
maildb = require('../maildb.js');
|
||||
MailError = mail.MailError,
|
||||
maildb = require('../maildb.js'),
|
||||
nock = require('nock'),
|
||||
settings = require('../settings.js');
|
||||
|
||||
const DOMAIN_0 = {
|
||||
domain: 'example.com',
|
||||
@@ -22,6 +26,10 @@ const DOMAIN_0 = {
|
||||
tlsConfig: { provider: 'fallback' }
|
||||
};
|
||||
|
||||
const APPSTORE_USER_ID = 'appstoreuserid';
|
||||
const APPSTORE_TOKEN = 'appstoretoken';
|
||||
const CLOUDRON_ID = 'cloudronid';
|
||||
|
||||
function setup(done) {
|
||||
config._reset();
|
||||
config.set('fqdn', 'example.com');
|
||||
@@ -30,13 +38,27 @@ function setup(done) {
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear,
|
||||
settings.initialize,
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain)
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain),
|
||||
function (callback) {
|
||||
var scope = nock('http://localhost:6060')
|
||||
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(201, { cloudron: { id: CLOUDRON_ID }});
|
||||
|
||||
settings.setAppstoreConfig({ userId: APPSTORE_USER_ID, token: APPSTORE_TOKEN }, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
async.series([
|
||||
settings.uninitialize,
|
||||
database._clear,
|
||||
database.uninitialize
|
||||
], done);
|
||||
@@ -46,6 +68,8 @@ describe('Mail', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
beforeEach(nock.cleanAll);
|
||||
|
||||
describe('values', function () {
|
||||
it('can get default', function (done) {
|
||||
mail.getDomain(DOMAIN_0.domain, function (error, mailConfig) {
|
||||
@@ -97,9 +121,27 @@ describe('Mail', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot enable mail without a subscription', function (done) {
|
||||
var scope = nock('http://localhost:6060')
|
||||
.get(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/subscription?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(200, { subscription: { id: 'free', plan: { id: 'free' }}});
|
||||
|
||||
mail.setMailEnabled(DOMAIN_0.domain, true, function (error) {
|
||||
expect(error).to.be.a(MailError);
|
||||
expect(error.reason).to.equal(MailError.BILLING_REQUIRED);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can enable mail', function (done) {
|
||||
var scope = nock('http://localhost:6060')
|
||||
.get(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/subscription?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(200, { subscription: { id: 'basic', plan: { id: 'basic' }}});
|
||||
|
||||
mail.setMailEnabled(DOMAIN_0.domain, true, function (error) {
|
||||
expect(error).to.be(null);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
|
||||
mail.getDomain(DOMAIN_0.domain, function (error, mailConfig) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
@@ -121,7 +121,7 @@ describe('Certificates', function () {
|
||||
|
||||
async.series([
|
||||
setup,
|
||||
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
|
||||
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('Certificates', function () {
|
||||
|
||||
async.series([
|
||||
setup,
|
||||
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
|
||||
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -183,7 +183,7 @@ describe('Certificates', function () {
|
||||
|
||||
async.series([
|
||||
setup,
|
||||
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
|
||||
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ function checkAppUpdates(callback) {
|
||||
}
|
||||
|
||||
// always send notifications if user is on the free plan
|
||||
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
|
||||
if (appstore.isFreePlan(result)) {
|
||||
debug('Notifying user of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version);
|
||||
mailer.appUpdateAvailable(app, false /* subscription */, updateInfo);
|
||||
return iteratorDone();
|
||||
@@ -162,7 +162,7 @@ function checkBoxUpdates(callback) {
|
||||
}
|
||||
|
||||
// always send notifications if user is on the free plan
|
||||
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
|
||||
if (appstore.isFreePlan(result)) {
|
||||
mailer.boxUpdateAvailable(false /* hasSubscription */, updateInfo.version, updateInfo.changelog);
|
||||
return done();
|
||||
}
|
||||
|
||||
+5
-2
@@ -220,7 +220,10 @@ function verify(userId, password, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// for just invited users the username may be still null
|
||||
if (user.username && verifyGhost(user.username, password)) return callback(null, user);
|
||||
if (user.username && verifyGhost(user.username, password)) {
|
||||
user.ghost = true;
|
||||
return callback(null, user);
|
||||
}
|
||||
|
||||
var saltBinary = new Buffer(user.salt, 'hex');
|
||||
crypto.pbkdf2(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST, function (error, derivedKey) {
|
||||
@@ -562,7 +565,7 @@ function setTwoFactorAuthenticationSecret(userId, callback) {
|
||||
|
||||
if (result.twoFactorAuthenticationEnabled) return callback(new UsersError(UsersError.ALREADY_EXISTS));
|
||||
|
||||
var secret = speakeasy.generateSecret({ name: `Cloudron (${config.adminFqdn()})` });
|
||||
var secret = speakeasy.generateSecret({ name: `Cloudron ${config.adminFqdn()} (${result.username})` });
|
||||
|
||||
userdb.update(userId, { twoFactorAuthenticationSecret: secret.base32, twoFactorAuthenticationEnabled: false }, function (error) {
|
||||
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
|
||||
|
||||
Reference in New Issue
Block a user