Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b3edf6efc | |||
| 07e649a2d3 | |||
| 8c63b6716d | |||
| 6fd314fe82 | |||
| 0c7eaf09a9 | |||
| d0988e2d61 | |||
| 4bedbd7167 | |||
| 7ca7901a73 | |||
| d28dfdbd03 | |||
| c85ca3c6e2 | |||
| da934d26af | |||
| f7cc49c5f4 | |||
| 27e263e7fb | |||
| 052050f48b | |||
| 81e29c7c2b | |||
| c3fbead658 | |||
| 36f5b6d678 | |||
| a45b1449de | |||
| a1020ec6b8 | |||
| d384284ec8 | |||
| bd29447a7f | |||
| aa5952fe0b | |||
| 39dc5da05a | |||
| d0e07d995a | |||
| 94408c1c3d | |||
| 66f032a7ee | |||
| 4356df3676 | |||
| 1e730d2fc0 | |||
| e8875ccd2e | |||
| 2b3656404b | |||
| 60b5e6f711 | |||
| b9166b382d | |||
| d0c427b0df | |||
| da5d0c61b4 | |||
| 1f75c2cc48 | |||
| d0197aab15 | |||
| e4a70b95f5 | |||
| f4d3d79922 | |||
| e3827ee25f | |||
| 9981ff2495 | |||
| 722b14b13d | |||
| eb2fb6491c | |||
| a53afbce91 | |||
| 31af6c64d0 | |||
| e8efc5a1b2 | |||
| 0c07c6e4d0 | |||
| da5fd71aaa |
@@ -1264,3 +1264,20 @@
|
||||
* Enhance user creation API to take a password
|
||||
* Relax restriction on mailbox names now that it is decoupled from user management
|
||||
|
||||
[2.2.1]
|
||||
* Add 2FA support for the admin dashboard
|
||||
* Add Gandi & GoDaddy DNS providers
|
||||
* Fix zone detection logic on Route53 accounts with more than 100 zones
|
||||
* Warn using when disabling email
|
||||
* Cleanup scope management in REST API
|
||||
* 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
|
||||
|
||||
|
||||
@@ -4,10 +4,14 @@ exports.up = function(db, callback) {
|
||||
db.runSql('UPDATE clients SET scope=? WHERE id=? OR id=? OR id=?', ['*', 'cid-webadmin', 'cid-sdk', 'cid-cli'], function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('UPDATE tokens SET scope=? WHERE scope LIKE ?', ['*', '%*%'], function (error) {
|
||||
db.runSql('UPDATE tokens SET scope=? WHERE scope LIKE ?', ['*', '%*%'], function (error) { // remove the roleSdk
|
||||
if (error) console.error(error);
|
||||
|
||||
callback(error);
|
||||
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1525636734905, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
|
||||
if (error) console.error(error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
+188
-105
@@ -22,11 +22,12 @@ var accesscontrol = require('./accesscontrol.js'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
ClientsError = clients.ClientsError,
|
||||
crypto = require('crypto'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:addons'),
|
||||
docker = require('./docker.js'),
|
||||
dockerConnection = docker.connection,
|
||||
fs = require('fs'),
|
||||
generatePassword = require('password-generator'),
|
||||
hat = require('hat'),
|
||||
infra = require('./infra_version.js'),
|
||||
mail = require('./mail.js'),
|
||||
@@ -364,23 +365,28 @@ function setupSendMail(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up SendMail');
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
appdb.getAddonConfigByName(app.id, 'sendmail', 'MAIL_SMTP_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
|
||||
var password = error ? hat(4 * 128) : existingPassword;
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -401,23 +407,28 @@ function setupRecvMail(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up recvmail');
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
appdb.getAddonConfigByName(app.id, 'recvmail', 'MAIL_IMAP_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
|
||||
var password = error ? hat(4 * 128) : existingPassword;
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_IMAP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_IMAP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -431,6 +442,15 @@ function teardownRecvMail(app, options, callback) {
|
||||
appdb.unsetAddonConfig(app.id, 'recvmail', callback);
|
||||
}
|
||||
|
||||
function mysqlDatabaseName(appId, prefix) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
|
||||
var md5sum = crypto.createHash('md5'); // get rid of "-"
|
||||
md5sum.update(appId);
|
||||
var dbname = md5sum.digest('hex').substring(0, 16); // max length of mysql usernames is 16
|
||||
return prefix ? `${dbname}_` : dbname;
|
||||
}
|
||||
|
||||
function setupMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
@@ -438,16 +458,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, options.multipleDatabases);
|
||||
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 +496,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, options.multipleDatabases);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', dbname ];
|
||||
|
||||
debugApp(app, 'Tearing down mysql');
|
||||
|
||||
@@ -479,7 +520,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, options.multipleDatabases);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', dbname ];
|
||||
|
||||
docker.execContainer('mysql', cmd, { stdout: output }, callback);
|
||||
}
|
||||
@@ -499,7 +541,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, options.multipleDatabases);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', dbname ];
|
||||
docker.execContainer('mysql', cmd, { stdin: input }, callback);
|
||||
});
|
||||
}
|
||||
@@ -511,16 +554,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 +585,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 +610,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 +631,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 +645,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 +677,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 +701,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 +722,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 +735,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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
'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/gandi'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
var GANDI_API = 'https://dns.api.gandi.net/api/v5';
|
||||
|
||||
function formatError(response) {
|
||||
return util.format(`Gandi DNS error [${response.statusCode}] ${response.body.message}`);
|
||||
}
|
||||
|
||||
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(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
var data = {
|
||||
'rrset_ttl': 300, // this is the minimum allowed
|
||||
'rrset_values': values // for mx records, value is already of the '<priority> <server>' format
|
||||
};
|
||||
|
||||
superagent.put(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
|
||||
.set('X-Api-Key', dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.send(data)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result)));
|
||||
if (result.statusCode !== 201) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null, 'unused-id');
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
|
||||
|
||||
superagent.get(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
|
||||
.set('X-Api-Key', dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 404) return callback(null, [ ]);
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('get: %j', result.body);
|
||||
|
||||
return callback(null, result.body.rrset_values);
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
superagent.del(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
|
||||
.set('X-Api-Key', dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(null);
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 204) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('del: done');
|
||||
|
||||
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 = {
|
||||
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('.gandi.net') !== -1; })) {
|
||||
debug('verifyDnsConfig: %j does not contain Gandi NS', nameservers);
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Gandi'));
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+3
-3
@@ -174,14 +174,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !resolvedNS) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
getZoneByName(credentials, zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var definedNS = zone.metadata.nameServers.sort().map(function(r) { return r.replace(/\.$/, ''); });
|
||||
if (!_.isEqual(definedNS, resolvedNS.sort())) {
|
||||
debug('verifyDnsConfig: %j and %j do not match', resolvedNS, definedNS);
|
||||
if (!_.isEqual(definedNS, nameservers.sort())) {
|
||||
debug('verifyDnsConfig: %j and %j do not match', nameservers, definedNS);
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
'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/godaddy'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
// const GODADDY_API_OTE = 'https://api.ote-godaddy.com/v1/domains';
|
||||
const GODADDY_API = 'https://api.godaddy.com/v1/domains';
|
||||
|
||||
// this is a workaround for godaddy not having a delete API
|
||||
// https://stackoverflow.com/questions/39347464/delete-record-libcloud-godaddy-api
|
||||
const GODADDY_INVALID_IP = '0.0.0.0';
|
||||
|
||||
function formatError(response) {
|
||||
return util.format(`GoDaddy DNS error [${response.statusCode}] ${response.body.message}`);
|
||||
}
|
||||
|
||||
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(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
var records = [ ];
|
||||
values.forEach(function (value) {
|
||||
var record = { ttl: 600 }; // 600 is the min ttl
|
||||
|
||||
if (type === 'MX') {
|
||||
record.priority = parseInt(value.split(' ')[0], 10);
|
||||
record.data = value.split(' ')[1];
|
||||
} else {
|
||||
record.data = value;
|
||||
}
|
||||
|
||||
records.push(record);
|
||||
});
|
||||
|
||||
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
|
||||
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
|
||||
.timeout(30 * 1000)
|
||||
.send(records)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result))); // no such zone
|
||||
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result))); // conflict
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null, 'unused-id');
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
|
||||
|
||||
superagent.get(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
|
||||
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 404) return callback(null, [ ]);
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('get: %j', result.body);
|
||||
|
||||
var values = result.body.map(function (record) { return record.data; });
|
||||
|
||||
if (values.length === 1 && values[0] === GODADDY_INVALID_IP) return callback(null, [ ]); // pretend this record doesn't exist
|
||||
|
||||
return callback(null, values);
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`get: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
if (type !== 'A') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Not supported by GoDaddy API'))); // can never happen
|
||||
|
||||
// check if the record exists at all so that we don't insert the "Dead" record for no reason
|
||||
get(dnsConfig, zoneName, subdomain, type, function (error, values) {
|
||||
if (error) return callback(error);
|
||||
if (values.length === 0) return callback();
|
||||
|
||||
// godaddy does not have a delete API. so fill it up with an invalid IP that we can ignore in future get()
|
||||
var records = [{
|
||||
ttl: 600,
|
||||
data: GODADDY_INVALID_IP
|
||||
}];
|
||||
|
||||
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
|
||||
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
|
||||
.send(records)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(null);
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('del: done');
|
||||
|
||||
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 = {
|
||||
apiKey: dnsConfig.apiKey,
|
||||
apiSecret: dnsConfig.apiSecret
|
||||
};
|
||||
|
||||
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('.domaincontrol.com') !== -1; })) {
|
||||
debug('verifyDnsConfig: %j does not contain GoDaddy NS', nameservers);
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to GoDaddy'));
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+2
-1
@@ -57,7 +57,8 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
|
||||
// Very basic check if the nameservers can be fetched
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to get nameservers'));
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
callback(null, { wildcard: !!dnsConfig.wildcard });
|
||||
});
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
'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,
|
||||
answer: values[0],
|
||||
ttl: 300 // 300 is the lowest
|
||||
};
|
||||
|
||||
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,
|
||||
answer: values[0],
|
||||
ttl: 300 // 300 is the lowest
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+14
-4
@@ -39,7 +39,16 @@ function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.listHostedZones({}, function (error, result) {
|
||||
|
||||
// backward compat for 2.2, where we only required access to "listHostedZones"
|
||||
let listHostedZones;
|
||||
if (dnsConfig.listHostedZonesByName) {
|
||||
listHostedZones = route53.listHostedZonesByName.bind(route53, { MaxItems: '1', DNSName: zoneName + '.' });
|
||||
} else {
|
||||
listHostedZones = route53.listHostedZones.bind(route53, {}); // currently, this route does not support > 100 zones
|
||||
}
|
||||
|
||||
listHostedZones(function (error, result) {
|
||||
if (error && error.code === 'AccessDenied') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
@@ -87,7 +96,7 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
|
||||
var records = values.map(function (v) { return { Value: v }; });
|
||||
var records = values.map(function (v) { return { Value: v }; }); // for mx records, value is already of the '<priority> <server>' format
|
||||
|
||||
var params = {
|
||||
ChangeBatch: {
|
||||
@@ -228,7 +237,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
accessKeyId: dnsConfig.accessKeyId,
|
||||
secretAccessKey: dnsConfig.secretAccessKey,
|
||||
region: dnsConfig.region || 'us-east-1',
|
||||
endpoint: dnsConfig.endpoint || null
|
||||
endpoint: dnsConfig.endpoint || null,
|
||||
listHostedZonesByName: true // new/updated creds require this perm
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
@@ -252,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');
|
||||
|
||||
+15
-5
@@ -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, 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) {
|
||||
@@ -80,6 +80,9 @@ function api(provider) {
|
||||
case 'route53': return require('./dns/route53.js');
|
||||
case 'gcdns': return require('./dns/gcdns.js');
|
||||
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;
|
||||
@@ -133,7 +136,7 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
|
||||
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));
|
||||
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record: ' + error.message));
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
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));
|
||||
@@ -186,8 +189,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');
|
||||
@@ -198,6 +202,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));
|
||||
@@ -210,7 +220,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));
|
||||
@@ -218,7 +228,7 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
domaindb.update(domain, { provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
|
||||
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
exports = module.exports = {
|
||||
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
|
||||
// a minor version makes all apps re-configure themselves
|
||||
'version': '48.9.0',
|
||||
'version': '48.10.0',
|
||||
|
||||
'baseImages': [ 'cloudron/base:0.10.0' ],
|
||||
|
||||
// Note that if any of the databases include an upgrade, bump the infra version above
|
||||
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
|
||||
'images': {
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.0.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.0.0' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.0.1' },
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.1.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.1.0' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.1.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.2.2' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.3.0' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
|
||||
}
|
||||
};
|
||||
|
||||
+30
-3
@@ -22,6 +22,7 @@ var apps = require('./apps.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
util = require('util'),
|
||||
@@ -47,8 +48,15 @@ function start(callback) {
|
||||
// short-circuit for the restart case
|
||||
if (_.isEqual(infra, existingInfra)) {
|
||||
debug('platform is uptodate at version %s', infra.version);
|
||||
emitPlatformReady();
|
||||
return callback();
|
||||
|
||||
updateAddons(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
emitPlatformReady();
|
||||
|
||||
callback();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
debug('Updating infrastructure from %s to %s', existingInfra.version, infra.version);
|
||||
@@ -61,7 +69,8 @@ function start(callback) {
|
||||
startAddons.bind(null, existingInfra),
|
||||
removeOldImages,
|
||||
startApps.bind(null, existingInfra),
|
||||
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra))
|
||||
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4)),
|
||||
updateAddons
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -80,6 +89,24 @@ function stop(callback) {
|
||||
taskmanager.pauseTasks(callback);
|
||||
}
|
||||
|
||||
function updateAddons(callback) {
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
for (var containerName of [ 'mysql', 'postgresql', 'mail', 'mongodb' ]) {
|
||||
const containerConfig = platformConfig[containerName];
|
||||
if (!containerConfig) continue;
|
||||
|
||||
if (!containerConfig.memory || !containerConfig.memorySwap) continue;
|
||||
|
||||
const cmd = `docker update --memory ${containerConfig.memory} --memory-swap ${containerConfig.memorySwap} ${containerName}`;
|
||||
shell.execSync(`update${containerName}`, cmd);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function emitPlatformReady() {
|
||||
// give some time for the platform to "settle". For example, mysql might still be initing the
|
||||
// database dir and we cannot call service scripts until that's done.
|
||||
|
||||
@@ -17,7 +17,7 @@ function login(req, res, next) {
|
||||
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
|
||||
if (user.twoFactorAuthenticationEnabled) {
|
||||
if (!user.ghost && user.twoFactorAuthenticationEnabled) {
|
||||
if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided'));
|
||||
|
||||
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken });
|
||||
|
||||
@@ -67,6 +67,7 @@ function update(req, res, next) {
|
||||
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be an object'));
|
||||
if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
|
||||
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
|
||||
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
|
||||
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.cert || typeof req.body.fallbackCertificate.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
||||
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.key || typeof req.body.fallbackCertificate.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
||||
@@ -76,7 +77,7 @@ function update(req, res, next) {
|
||||
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
|
||||
req.clearTimeout();
|
||||
|
||||
domains.update(req.params.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
|
||||
domains.update(req.params.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -372,7 +372,7 @@ describe('Clients', function () {
|
||||
setup,
|
||||
|
||||
function (callback) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
@@ -536,7 +536,7 @@ describe('Clients', function () {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
// further calls with this token should not work
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
@@ -192,7 +192,7 @@ describe('Developer API', function () {
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
|
||||
secret = result.body.secret;
|
||||
callback(error);
|
||||
});
|
||||
@@ -203,7 +203,7 @@ describe('Developer API', function () {
|
||||
encoding: 'base32'
|
||||
});
|
||||
|
||||
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
@@ -213,7 +213,7 @@ describe('Developer API', function () {
|
||||
after(function (done) {
|
||||
async.series([
|
||||
function (callback) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error, result) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error, result) {
|
||||
callback(error);
|
||||
});
|
||||
},
|
||||
@@ -285,14 +285,14 @@ describe('Developer API', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('fails with non sdk token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password').query({ access_token: token_normal }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password').query({ access_token: token_normal }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password').query({ access_token: token_sdk }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password').query({ access_token: token_sdk }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ function setup(done) {
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
|
||||
@@ -601,7 +601,7 @@ describe('OAuth2', function () {
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
|
||||
secret = result.body.secret;
|
||||
callback(error);
|
||||
});
|
||||
@@ -612,7 +612,7 @@ describe('OAuth2', function () {
|
||||
encoding: 'base32'
|
||||
});
|
||||
|
||||
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
|
||||
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
@@ -865,7 +865,7 @@ describe('OAuth2', function () {
|
||||
expect(foo.token_type).to.eql('Bearer');
|
||||
|
||||
// Ensure the token is also usable
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile?access_token=' + foo.access_token, function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile?access_token=' + foo.access_token, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.status).to.eql(200);
|
||||
expect(result.body.username).to.equal(USER_0.username.toLowerCase());
|
||||
@@ -1252,7 +1252,7 @@ describe('OAuth2', function () {
|
||||
expect(body.token_type).to.eql('Bearer');
|
||||
|
||||
// Ensure the token is also usable
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile?access_token=' + body.access_token, function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile?access_token=' + body.access_token, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.status).to.eql(200);
|
||||
expect(result.body.username).to.equal(USER_0.username.toLowerCase());
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('Profile API', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile/').end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
@@ -81,7 +81,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('fails with empty token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile/').query({ access_token: '' }).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: '' }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
@@ -89,7 +89,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('fails with invalid token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile/').query({ access_token: 'some token' }).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: 'some token' }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
@@ -97,7 +97,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile/').query({ access_token: token_0 }).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: token_0 }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
|
||||
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
|
||||
@@ -120,7 +120,7 @@ describe('Profile API', function () {
|
||||
tokendb.add(token, user_0.id, null, expires, accesscontrol.SCOPE_ANY, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile').query({ access_token: token }).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
@@ -129,14 +129,14 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('fails with invalid token in auth header', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile').set('Authorization', 'Bearer ' + 'x' + token_0).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile').set('Authorization', 'Bearer ' + 'x' + token_0).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with token in auth header', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile').set('Authorization', 'Bearer ' + token_0).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile').set('Authorization', 'Bearer ' + token_0).end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
|
||||
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
|
||||
@@ -154,7 +154,7 @@ describe('Profile API', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('change email fails due to missing token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile')
|
||||
.send({ email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
@@ -163,7 +163,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('change email fails due to invalid email', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ email: 'foo@bar' })
|
||||
.end(function (error, result) {
|
||||
@@ -173,7 +173,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('change user succeeds without email nor displayName', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.send({})
|
||||
.end(function (error, result) {
|
||||
@@ -183,13 +183,13 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('change email succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ email: EMAIL_0_NEW, fallbackEmail: EMAIL_0_NEW_FALLBACK })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -205,13 +205,13 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('change displayName succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ displayName: DISPLAY_NAME_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token_0 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -231,7 +231,7 @@ describe('Profile API', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('fails due to missing current password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ newPassword: 'some wrong password' })
|
||||
.end(function (err, res) {
|
||||
@@ -241,7 +241,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('fails due to missing new password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
@@ -251,7 +251,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('fails due to wrong password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
@@ -261,7 +261,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('fails due to invalid password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: PASSWORD, newPassword: 'five' })
|
||||
.end(function (err, res) {
|
||||
@@ -271,7 +271,7 @@ describe('Profile API', function () {
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
|
||||
superagent.post(SERVER_URL + '/api/v1/profile/password')
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('Users API', function () {
|
||||
// stash for later use
|
||||
token = res.body.token;
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile').query({ access_token: token }).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
|
||||
expect(error).to.eql(null);
|
||||
expect(result.status).to.equal(200);
|
||||
|
||||
@@ -703,7 +703,7 @@ describe('Users API', function () {
|
||||
});
|
||||
|
||||
it('can get profile of user with pre-set password', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/user/profile')
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
|
||||
+6
-7
@@ -131,13 +131,12 @@ function initializeExpressSync() {
|
||||
// working off the user behind the provided token
|
||||
router.get ('/api/v1/user/apps', profileScope, routes.apps.getAllByUser);
|
||||
router.get ('/api/v1/user/cloudron_config', profileScope, routes.user.getCloudronConfig);
|
||||
router.get ('/api/v1/profile', profileScope, routes.profile.get); // duplicate route for compatibility
|
||||
router.get ('/api/v1/user/profile', profileScope, routes.profile.get);
|
||||
router.post('/api/v1/user/profile', profileScope, routes.profile.update);
|
||||
router.post('/api/v1/user/profile/password', profileScope, routes.users.verifyPassword, routes.profile.changePassword);
|
||||
router.post('/api/v1/user/profile/twofactorauthentication', profileScope, routes.profile.setTwoFactorAuthenticationSecret);
|
||||
router.post('/api/v1/user/profile/twofactorauthentication/enable', profileScope, routes.profile.enableTwoFactorAuthentication);
|
||||
router.post('/api/v1/user/profile/twofactorauthentication/disable', profileScope, routes.users.verifyPassword, routes.profile.disableTwoFactorAuthentication);
|
||||
router.get ('/api/v1/profile', profileScope, routes.profile.get);
|
||||
router.post('/api/v1/profile', profileScope, routes.profile.update);
|
||||
router.post('/api/v1/profile/password', profileScope, routes.users.verifyPassword, routes.profile.changePassword);
|
||||
router.post('/api/v1/profile/twofactorauthentication', profileScope, routes.profile.setTwoFactorAuthenticationSecret);
|
||||
router.post('/api/v1/profile/twofactorauthentication/enable', profileScope, routes.profile.enableTwoFactorAuthentication);
|
||||
router.post('/api/v1/profile/twofactorauthentication/disable', profileScope, routes.users.verifyPassword, routes.profile.disableTwoFactorAuthentication);
|
||||
|
||||
// user routes
|
||||
router.get ('/api/v1/users', usersScope, routes.users.list);
|
||||
|
||||
+29
-1
@@ -35,6 +35,9 @@ exports = module.exports = {
|
||||
getEmailDigest: getEmailDigest,
|
||||
setEmailDigest: setEmailDigest,
|
||||
|
||||
getPlatformConfig: getPlatformConfig,
|
||||
setPlatformConfig: setPlatformConfig,
|
||||
|
||||
getAll: getAll,
|
||||
|
||||
// booleans. if you add an entry here, be sure to fix getAll
|
||||
@@ -46,6 +49,7 @@ exports = module.exports = {
|
||||
UPDATE_CONFIG_KEY: 'update_config',
|
||||
APPSTORE_CONFIG_KEY: 'appstore_config',
|
||||
CAAS_CONFIG_KEY: 'caas_config',
|
||||
PLATFORM_CONFIG_KEY: 'platform_config',
|
||||
|
||||
// strings
|
||||
APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern',
|
||||
@@ -88,6 +92,7 @@ var gDefaults = (function () {
|
||||
result[exports.APPSTORE_CONFIG_KEY] = {};
|
||||
result[exports.CAAS_CONFIG_KEY] = {};
|
||||
result[exports.EMAIL_DIGEST] = true;
|
||||
result[exports.PLATFORM_CONFIG_KEY] = {};
|
||||
|
||||
return result;
|
||||
})();
|
||||
@@ -371,6 +376,29 @@ function getAppstoreConfig(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getPlatformConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.PLATFORM_CONFIG_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.PLATFORM_CONFIG_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, JSON.parse(value));
|
||||
});
|
||||
}
|
||||
|
||||
function setPlatformConfig(platformConfig, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.set(exports.PLATFORM_CONFIG_KEY, JSON.stringify(platformConfig), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.PLATFORM_CONFIG_KEY, platformConfig);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function setAppstoreConfig(appstoreConfig, callback) {
|
||||
assert.strictEqual(typeof appstoreConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -443,7 +471,7 @@ function getAll(callback) {
|
||||
result[exports.DYNAMIC_DNS_KEY] = !!result[exports.DYNAMIC_DNS_KEY];
|
||||
|
||||
// convert JSON objects
|
||||
[exports.BACKUP_CONFIG_KEY, exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY ].forEach(function (key) {
|
||||
[exports.BACKUP_CONFIG_KEY, exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY ].forEach(function (key) {
|
||||
result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ describe('apptask', function () {
|
||||
nock.cleanAll();
|
||||
|
||||
var awsScope = nock('http://localhost:5353')
|
||||
.get('/2013-04-01/hostedzone')
|
||||
.get('/2013-04-01/hostedzonesbyname?dnsname=example.com.&maxitems=1')
|
||||
.times(2)
|
||||
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }))
|
||||
.get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=applocation.' + DOMAIN_0.domain + '.&type=A')
|
||||
@@ -258,7 +258,7 @@ describe('apptask', function () {
|
||||
nock.cleanAll();
|
||||
|
||||
var awsScope = nock('http://localhost:5353')
|
||||
.get('/2013-04-01/hostedzone')
|
||||
.get('/2013-04-01/hostedzonesbyname?dnsname=example.com.&maxitems=1')
|
||||
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }))
|
||||
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
|
||||
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } }));
|
||||
|
||||
@@ -95,6 +95,8 @@ const TEST_DOMAIN = {
|
||||
};
|
||||
|
||||
describe('database', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
before(function (done) {
|
||||
config._reset();
|
||||
config.setFqdn(TEST_DOMAIN.domain);
|
||||
|
||||
+261
-5
@@ -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) {
|
||||
@@ -341,6 +341,262 @@ describe('dns provider', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('godaddy', function () {
|
||||
var KEY = 'somekey', SECRET = 'somesecret';
|
||||
var GODADDY_API = 'https://api.godaddy.com/v1/domains';
|
||||
|
||||
before(function (done) {
|
||||
DOMAIN_0.provider = 'godaddy';
|
||||
DOMAIN_0.config = {
|
||||
apiKey: KEY,
|
||||
apiSecret: SECRET
|
||||
};
|
||||
|
||||
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 = [{
|
||||
ttl: 600,
|
||||
data: '1.2.3.4'
|
||||
}];
|
||||
|
||||
var req1 = nock(GODADDY_API)
|
||||
.put('/' + DOMAIN_0.zoneName + '/records/A/test', 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();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var DOMAIN_RECORD_0 = [{
|
||||
ttl: 600,
|
||||
data: '1.2.3.4'
|
||||
}];
|
||||
|
||||
var req1 = nock(GODADDY_API)
|
||||
.get('/' + DOMAIN_0.zoneName + '/records/A/test')
|
||||
.reply(200, 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[0].data);
|
||||
expect(req1.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('del succeeds', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var DOMAIN_RECORD_0 = [{ // existing
|
||||
ttl: 600,
|
||||
data: '1.2.3.4'
|
||||
}];
|
||||
|
||||
var DOMAIN_RECORD_1 = [{ // replaced
|
||||
ttl: 600,
|
||||
data: '0.0.0.0'
|
||||
}];
|
||||
|
||||
var req1 = nock(GODADDY_API)
|
||||
.get('/' + DOMAIN_0.zoneName + '/records/A/test')
|
||||
.reply(200, DOMAIN_RECORD_0);
|
||||
|
||||
var req2 = nock(GODADDY_API)
|
||||
.put('/' + DOMAIN_0.zoneName + '/records/A/test', DOMAIN_RECORD_1)
|
||||
.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('gandi', function () {
|
||||
var TOKEN = 'sometoken';
|
||||
var GANDI_API = 'https://dns.api.gandi.net/api/v5';
|
||||
|
||||
before(function (done) {
|
||||
DOMAIN_0.provider = 'gandi';
|
||||
DOMAIN_0.config = {
|
||||
token: TOKEN
|
||||
};
|
||||
|
||||
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) {
|
||||
nock.cleanAll();
|
||||
|
||||
var DOMAIN_RECORD_0 = {
|
||||
'rrset_ttl': 300,
|
||||
'rrset_values': [ '1.2.3.4' ]
|
||||
};
|
||||
|
||||
var req1 = nock(GANDI_API)
|
||||
.put('/domains/' + DOMAIN_0.zoneName + '/records/test/A', DOMAIN_RECORD_0)
|
||||
.reply(201, { message: 'Zone Record Created' });
|
||||
|
||||
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) {
|
||||
expect(error).to.eql(null);
|
||||
expect(req1.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var DOMAIN_RECORD_0 = {
|
||||
'rrset_type': 'A',
|
||||
'rrset_ttl': 600,
|
||||
'rrset_name': 'test',
|
||||
'rrset_values': [ '1.2.3.4' ]
|
||||
};
|
||||
|
||||
var req1 = nock(GANDI_API)
|
||||
.get('/domains/' + DOMAIN_0.zoneName + '/records/test/A')
|
||||
.reply(200, 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.rrset_values[0]);
|
||||
expect(req1.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('del succeeds', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var req2 = nock(GANDI_API)
|
||||
.delete('/domains/' + DOMAIN_0.zoneName + '/records/test/A')
|
||||
.reply(204, { });
|
||||
|
||||
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
|
||||
expect(error).to.eql(null);
|
||||
expect(req2.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 = [];
|
||||
@@ -412,14 +668,14 @@ describe('dns provider', function () {
|
||||
Route53Mock.prototype.getChange = mockery(awsAnswerQueue);
|
||||
Route53Mock.prototype.changeResourceRecordSets = mockery(awsAnswerQueue);
|
||||
Route53Mock.prototype.listResourceRecordSets = mockery(awsAnswerQueue);
|
||||
Route53Mock.prototype.listHostedZones = mockery(awsAnswerQueue);
|
||||
Route53Mock.prototype.listHostedZonesByName = mockery(awsAnswerQueue);
|
||||
|
||||
// override route53 in AWS
|
||||
// Comment this out and replace the config with real tokens to test against AWS proper
|
||||
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 () {
|
||||
@@ -575,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 () {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to invalid username', function (done) {
|
||||
users.create('moo-daemon', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
users.create('moo+daemon', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.BAD_FIELD);
|
||||
@@ -198,8 +198,8 @@ describe('User', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to reserved pattern', function (done) {
|
||||
users.create('maybe-app', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
it('fails due to reserved app pattern', function (done) {
|
||||
users.create('maybe.app', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.BAD_FIELD);
|
||||
|
||||
+6
-3
@@ -91,8 +91,8 @@ function validateUsername(username) {
|
||||
|
||||
if (constants.RESERVED_NAMES.indexOf(username) !== -1) return new UsersError(UsersError.BAD_FIELD, 'Username is reserved');
|
||||
|
||||
// +/- can be tricky in emails. also need to consider valid LDAP characters here (e.g '+' is reserved)
|
||||
if (/[^a-zA-Z0-9.]/.test(username)) return new UsersError(UsersError.BAD_FIELD, 'Username can only contain alphanumerals and dot');
|
||||
// also need to consider valid LDAP characters here (e.g '+' is reserved)
|
||||
if (/[^a-zA-Z0-9.-]/.test(username)) return new UsersError(UsersError.BAD_FIELD, 'Username can only contain alphanumerals, dot and -');
|
||||
|
||||
// app emails are sent using the .app suffix
|
||||
if (username.indexOf('.app') !== -1) return new UsersError(UsersError.BAD_FIELD, 'Username pattern is reserved for apps');
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user