Compare commits

..

59 Commits

Author SHA1 Message Date
Girish Ramakrishnan 70128458b2 Fix crash when renewAll is called when cloudron is not setup yet 2018-06-05 21:27:32 -07:00
Girish Ramakrishnan 900225957e typo: code should return SetupError 2018-06-05 21:19:47 -07:00
Girish Ramakrishnan fd8f5e3c71 Return error for trailing dot instead 2018-06-05 21:09:07 -07:00
Girish Ramakrishnan 7382ea2b04 Handle my subdomain already existing 2018-06-05 20:53:28 -07:00
Girish Ramakrishnan 09163b8a2b strip any trailing dot in the domain and zoneName 2018-06-05 20:33:14 -07:00
Girish Ramakrishnan 953398c427 lint 2018-06-05 20:02:47 -07:00
Girish Ramakrishnan 9f7406c235 cloudron-activate: Add option to setup backup dir 2018-06-05 19:40:46 -07:00
Girish Ramakrishnan 2e427aa60e Add 2.3.2 changes 2018-06-05 09:51:56 -07:00
Girish Ramakrishnan ab80cc9ea1 Add username to the TOTP secret name
This works around issue in FreeOTP app which crashed when
the same name is used.

https://github.com/freeotp/freeotp-ios/issues/69
https://github.com/freeotp/freeotp-android/issues/69
2018-06-04 16:08:03 -07:00
Girish Ramakrishnan 321f11c644 mysql: _ prefix is hardcoded in mysql addon already
Fixes #560
2018-06-04 12:31:40 -07:00
Girish Ramakrishnan 47f85434db cloudron-activate: always login since activate return token and not accessToken 2018-06-01 00:12:19 -07:00
Girish Ramakrishnan 7717c7b1cd Add cloudron-activate script to automate activation from VM image 2018-05-31 23:46:44 -07:00
Johannes Zellner 7618aa786c Handle AppstoreError properly when no appstore account was set 2018-05-30 20:33:58 +02:00
Girish Ramakrishnan f752cb368c Remove spamcannibal
Fixes #559
2018-05-30 11:07:17 -07:00
Girish Ramakrishnan ca500e2165 mailer: do not send notifications to fallback email 2018-05-30 09:26:59 -07:00
Johannes Zellner 371f81b980 Add test for mail enabling without a subscription 2018-05-30 00:02:18 +02:00
Johannes Zellner c68cca9a54 Fixup mail test, which requires a subscription 2018-05-29 23:59:53 +02:00
Johannes Zellner 9194be06c3 Fix app purchase test 2018-05-29 23:24:08 +02:00
Johannes Zellner 9eb58cdfe5 Check for plan when enabling email 2018-05-29 13:31:43 +02:00
Johannes Zellner 99be89012d No need to check for active subscription state, as the appstore already does this 2018-05-29 13:31:43 +02:00
Johannes Zellner 541fabcb2e Add convenience function to determine if subscription is 'free' or not 2018-05-29 13:31:43 +02:00
Johannes Zellner 915e04eb08 We do not have an 'undecided' plan state anymore 2018-05-29 13:31:43 +02:00
Girish Ramakrishnan 48896d4e50 more changes 2018-05-28 10:06:46 -07:00
Johannes Zellner 29682c0944 Only allow max of 2 apps on the free plan 2018-05-26 18:53:20 +02:00
Girish Ramakrishnan 346b1cb91c more changes 2018-05-26 08:11:19 -07:00
Girish Ramakrishnan e552821c01 Add 2.3.1 changes 2018-05-25 11:44:04 -07:00
Girish Ramakrishnan bac3ba101e Add mailboxName to app configure route
Fixes #558
2018-05-24 16:26:34 -07:00
Girish Ramakrishnan 87c46fe3ea apps: return mailbox name as part of app
part of cloudron/box#558
2018-05-24 15:50:46 -07:00
Girish Ramakrishnan f9763b1ad3 namecom: MX record not set properly 2018-05-24 09:41:52 -07:00
Girish Ramakrishnan f1e6116b83 Fix copyright years 2018-05-23 20:02:33 -07:00
Girish Ramakrishnan 273948c3c7 Fix tests 2018-05-22 13:22:48 -07:00
Girish Ramakrishnan 9c073e7bee Preserve addons credentials when restoring 2018-05-22 13:07:58 -07:00
Girish Ramakrishnan 8b3edf6efc Bump mail container for managesieve fix 2018-05-18 18:26:19 -07:00
Girish Ramakrishnan 07e649a2d3 Add more changes 2018-05-17 20:17:24 -07:00
Girish Ramakrishnan 8c63b6716d Trigger a re-configure 2018-05-17 20:16:51 -07:00
Girish Ramakrishnan 6fd314fe82 Do not change password on app update
Fixes #554
2018-05-17 19:48:57 -07:00
Girish Ramakrishnan 0c7eaf09a9 bump container versions 2018-05-17 10:00:00 -07:00
Girish Ramakrishnan d0988e2d61 Generate password for mongodb on platform side
Part of #554
2018-05-17 10:00:00 -07:00
Girish Ramakrishnan 4bedbd7167 Generate password for postgresql on platform side
Part of #554
2018-05-17 10:00:00 -07:00
Girish Ramakrishnan 7ca7901a73 Generate password for mysql on platform side
Part of #554
2018-05-17 09:59:57 -07:00
Girish Ramakrishnan d28dfdbd03 Add 2.3.0 changes 2018-05-17 09:24:47 -07:00
Girish Ramakrishnan c85ca3c6e2 account setup simply redirects to main page now 2018-05-17 09:17:08 -07:00
Girish Ramakrishnan da934d26af call callback 2018-05-17 09:16:32 -07:00
Girish Ramakrishnan f7cc49c5f4 move platform config to db
this way it can be tied up to some REST API later

part of #555
2018-05-16 17:34:56 -07:00
Girish Ramakrishnan 27e263e7fb lint 2018-05-16 14:08:54 -07:00
Girish Ramakrishnan 052050f48b Add a way to persist addon memory configuration
Fixes #555
2018-05-16 14:00:55 -07:00
Girish Ramakrishnan 81e29c7c2b Make the INFRA_VERSION_FILE more readable 2018-05-16 09:54:42 -07:00
Girish Ramakrishnan c3fbead658 Allow zoneName to be changed in domain update route 2018-05-15 15:39:30 -07:00
Girish Ramakrishnan 36f5b6d678 manual dns: handle ENOTFOUND
Fixes #548
2018-05-15 15:39:18 -07:00
Girish Ramakrishnan a45b1449de Allow ghost users to skip 2fa 2018-05-14 15:07:01 -07:00
Girish Ramakrishnan a1020ec6b8 remove /user from profile route 2018-05-13 21:53:06 -07:00
Johannes Zellner d384284ec8 Add name.com DNS provider in the CHANGES file 2018-05-11 10:03:58 +02:00
Girish Ramakrishnan bd29447a7f gcdns: Fix typo 2018-05-10 10:05:42 -07:00
Johannes Zellner aa5952fe0b Wait longer for dns in apptask
name.com often takes longer to sync all nameservers, which means we
timeout too early for them
2018-05-10 15:37:47 +02:00
Johannes Zellner 39dc5da05a We have to return a value on dns record upserting 2018-05-09 18:58:09 +02:00
Johannes Zellner d0e07d995a Add name.com dns tests 2018-05-09 18:13:21 +02:00
Johannes Zellner 94408c1c3d Add name.com DNS provider 2018-05-09 18:13:14 +02:00
Girish Ramakrishnan 66f032a7ee route53: use credentials instead of dnsConfig 2018-05-07 23:41:03 -07:00
Girish Ramakrishnan 4356df3676 bump timeout 2018-05-07 16:28:11 -07:00
44 changed files with 1019 additions and 266 deletions
+25
View File
@@ -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
+1 -1
View File
@@ -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
+122
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 });
});
+243
View File
@@ -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
View File
@@ -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
View File
@@ -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));
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
<footer class="text-center">
<span class="text-muted">&copy; 2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted">&copy; 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
View File
@@ -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.
+1
View File
@@ -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
+2
View File
@@ -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) {
+1 -1
View File
@@ -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 });
+2 -1
View File
@@ -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));
+1
View File
@@ -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));
+2 -2
View File
@@ -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());
});
});
});
+1
View File
@@ -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));
+2 -2
View File
@@ -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);
+5 -5
View File
@@ -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();
});
+1 -1
View File
@@ -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();
+4 -4
View File
@@ -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());
+19 -19
View File
@@ -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) {
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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),
-1
View File
@@ -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
View File
@@ -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();
});
+7 -2
View File
@@ -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();
});
+2
View File
@@ -95,6 +95,8 @@ const TEST_DOMAIN = {
};
describe('database', function () {
this.timeout(5000);
before(function (done) {
config._reset();
config.setFqdn(TEST_DOMAIN.domain);
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+3 -3
View File
@@ -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);
});
+2 -2
View File
@@ -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
View File
@@ -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));