app.portBindings and newManifest.tcpPorts may be null
This commit is contained in:
768
src/addons.js
Normal file
768
src/addons.js
Normal file
@@ -0,0 +1,768 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
setupAddons: setupAddons,
|
||||
teardownAddons: teardownAddons,
|
||||
backupAddons: backupAddons,
|
||||
restoreAddons: restoreAddons,
|
||||
|
||||
getEnvironment: getEnvironment,
|
||||
getLinksSync: getLinksSync,
|
||||
getBindsSync: getBindsSync,
|
||||
|
||||
// exported for testing
|
||||
_allocateOAuthCredentials: allocateOAuthCredentials,
|
||||
_removeOAuthCredentials: removeOAuthCredentials
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
child_process = require('child_process'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:addons'),
|
||||
docker = require('./docker.js'),
|
||||
fs = require('fs'),
|
||||
generatePassword = require('password-generator'),
|
||||
hat = require('hat'),
|
||||
MemoryStream = require('memorystream'),
|
||||
once = require('once'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = child_process.spawn,
|
||||
tokendb = require('./tokendb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid'),
|
||||
vbox = require('./vbox.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
var NOOP = function (app, callback) { return callback(); };
|
||||
|
||||
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
|
||||
// teardown is destructive. app data stored with the addon is lost
|
||||
var KNOWN_ADDONS = {
|
||||
oauth: {
|
||||
setup: allocateOAuthCredentials,
|
||||
teardown: removeOAuthCredentials,
|
||||
backup: NOOP,
|
||||
restore: allocateOAuthCredentials
|
||||
},
|
||||
token: {
|
||||
setup: allocateAccessToken,
|
||||
teardown: removeAccessToken,
|
||||
backup: NOOP,
|
||||
restore: allocateAccessToken
|
||||
},
|
||||
ldap: {
|
||||
setup: setupLdap,
|
||||
teardown: teardownLdap,
|
||||
backup: NOOP,
|
||||
restore: setupLdap
|
||||
},
|
||||
sendmail: {
|
||||
setup: setupSendMail,
|
||||
teardown: teardownSendMail,
|
||||
backup: NOOP,
|
||||
restore: setupSendMail
|
||||
},
|
||||
mysql: {
|
||||
setup: setupMySql,
|
||||
teardown: teardownMySql,
|
||||
backup: backupMySql,
|
||||
restore: restoreMySql,
|
||||
},
|
||||
postgresql: {
|
||||
setup: setupPostgreSql,
|
||||
teardown: teardownPostgreSql,
|
||||
backup: backupPostgreSql,
|
||||
restore: restorePostgreSql
|
||||
},
|
||||
mongodb: {
|
||||
setup: setupMongoDb,
|
||||
teardown: teardownMongoDb,
|
||||
backup: backupMongoDb,
|
||||
restore: restoreMongoDb
|
||||
},
|
||||
redis: {
|
||||
setup: setupRedis,
|
||||
teardown: teardownRedis,
|
||||
backup: NOOP, // no backup because we store redis as part of app's volume
|
||||
restore: setupRedis // same thing
|
||||
},
|
||||
localstorage: {
|
||||
setup: NOOP, // docker creates the directory for us
|
||||
teardown: NOOP,
|
||||
backup: NOOP, // no backup because it's already inside app data
|
||||
restore: NOOP
|
||||
},
|
||||
_docker: {
|
||||
setup: NOOP,
|
||||
teardown: NOOP,
|
||||
backup: NOOP,
|
||||
restore: NOOP
|
||||
}
|
||||
};
|
||||
|
||||
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
var prefix = app ? (app.location || 'naked_domain') : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function setupAddons(app, addons, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addons || typeof addons === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!addons) return callback(null);
|
||||
|
||||
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
|
||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
|
||||
|
||||
debugApp(app, 'Setting up addon %s', addon);
|
||||
|
||||
KNOWN_ADDONS[addon].setup(app, iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function teardownAddons(app, addons, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addons || typeof addons === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!addons) return callback(null);
|
||||
|
||||
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
|
||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
|
||||
|
||||
debugApp(app, 'Tearing down addon %s', addon);
|
||||
|
||||
KNOWN_ADDONS[addon].teardown(app, iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function backupAddons(app, addons, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addons || typeof addons === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'backupAddons');
|
||||
|
||||
if (!addons) return callback(null);
|
||||
|
||||
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
|
||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
|
||||
|
||||
KNOWN_ADDONS[addon].backup(app, iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function restoreAddons(app, addons, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addons || typeof addons === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'restoreAddons');
|
||||
|
||||
if (!addons) return callback(null);
|
||||
|
||||
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
|
||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
|
||||
|
||||
KNOWN_ADDONS[addon].restore(app, iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function getEnvironment(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.getAddonConfigByAppId(app.id, callback);
|
||||
}
|
||||
|
||||
function getLinksSync(app, addons) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addons || typeof addons === 'object');
|
||||
|
||||
var links = [ ];
|
||||
|
||||
if (!addons) return links;
|
||||
|
||||
for (var addon in addons) {
|
||||
switch (addon) {
|
||||
case 'mysql': links.push('mysql:mysql'); break;
|
||||
case 'postgresql': links.push('postgresql:postgresql'); break;
|
||||
case 'sendmail': links.push('mail:mail'); break;
|
||||
case 'redis': links.push('redis-' + app.id + ':redis-' + app.id); break;
|
||||
case 'mongodb': links.push('mongodb:mongodb'); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
function getBindsSync(app, addons) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addons || typeof addons === 'object');
|
||||
|
||||
var binds = [ ];
|
||||
|
||||
if (!addons) return binds;
|
||||
|
||||
for (var addon in addons) {
|
||||
switch (addon) {
|
||||
case '_docker': binds.push('/var/run/docker.sock:/var/run/docker.sock:rw'); break;
|
||||
case 'localstorage': binds.push(path.join(paths.DATA_DIR, app.id, 'data') + ':/app/data:rw'); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
return binds;
|
||||
}
|
||||
|
||||
function allocateOAuthCredentials(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var appId = app.id;
|
||||
var id = 'cid-addon-' + uuid.v4();
|
||||
var clientSecret = hat(256);
|
||||
var redirectURI = 'https://' + config.appFqdn(app.location);
|
||||
var scope = 'profile,roleUser';
|
||||
|
||||
debugApp(app, 'allocateOAuthCredentials: id:%s clientSecret:%s', id, clientSecret);
|
||||
|
||||
clientdb.delByAppId('addon-' + appId, function (error) { // remove existing creds
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
clientdb.add(id, 'addon-' + appId, clientSecret, redirectURI, scope, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
'OAUTH_CLIENT_ID=' + id,
|
||||
'OAUTH_CLIENT_SECRET=' + clientSecret,
|
||||
'OAUTH_ORIGIN=' + config.adminOrigin()
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting oauth addon config to %j', env);
|
||||
|
||||
appdb.setAddonConfig(appId, 'oauth', env, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeOAuthCredentials(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'removeOAuthCredentials');
|
||||
|
||||
clientdb.delByAppId('addon-' + app.id, function (error) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'oauth', callback);
|
||||
});
|
||||
}
|
||||
|
||||
function setupLdap(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var env = [
|
||||
'LDAP_SERVER=172.17.42.1',
|
||||
'LDAP_PORT=3002',
|
||||
'LDAP_URL=ldap://172.17.42.1:3002',
|
||||
'LDAP_USERS_BASE_DN=ou=users,dc=cloudron',
|
||||
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron'
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up LDAP');
|
||||
|
||||
appdb.setAddonConfig(app.id, 'ldap', env, callback);
|
||||
}
|
||||
|
||||
function teardownLdap(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Tearing down LDAP');
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'ldap', callback);
|
||||
}
|
||||
|
||||
function setupSendMail(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var env = [
|
||||
'MAIL_SMTP_SERVER=mail',
|
||||
'MAIL_SMTP_PORT=25',
|
||||
'MAIL_SMTP_USERNAME=' + (app.location || app.id), // use app.id for bare domains
|
||||
'MAIL_DOMAIN=' + config.fqdn()
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up sendmail');
|
||||
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
}
|
||||
|
||||
function teardownSendMail(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Tearing down sendmail');
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'sendmail', callback);
|
||||
}
|
||||
|
||||
function setupMySql(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Setting up mysql');
|
||||
|
||||
var container = docker.getContainer('mysql');
|
||||
var cmd = [ '/addons/mysql/service.sh', 'add', app.id ];
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
if (error) return callback(error);
|
||||
|
||||
execContainer.start(function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var stdout = new MemoryStream();
|
||||
var stderr = new MemoryStream();
|
||||
|
||||
execContainer.modem.demuxStream(stream, stdout, stderr);
|
||||
stderr.on('data', function (data) { debugApp(app, data.toString('utf8')); }); // set -e output
|
||||
|
||||
var chunks = [ ];
|
||||
stdout.on('data', function (chunk) { chunks.push(chunk); });
|
||||
|
||||
stream.on('error', callback);
|
||||
stream.on('end', function () {
|
||||
var env = Buffer.concat(chunks).toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
debugApp(app, 'Setting mysql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mysql', env, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function teardownMySql(app, callback) {
|
||||
var container = docker.getContainer('mysql');
|
||||
var cmd = [ '/addons/mysql/service.sh', 'remove', app.id ];
|
||||
|
||||
debugApp(app, 'Tearing down mysql');
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
if (error) return callback(error);
|
||||
|
||||
execContainer.start(function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var data = '';
|
||||
stream.on('error', callback);
|
||||
stream.on('data', function (d) { data += d.toString('utf8'); });
|
||||
stream.on('end', function () {
|
||||
appdb.unsetAddonConfig(app.id, 'mysql', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backupMySql(app, callback) {
|
||||
debugApp(app, 'Backing up mysql');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', 'mysql', '/addons/mysql/service.sh', 'backup', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'backupMySql: done. code:%s signal:%s', code, signal);
|
||||
if (!callback.called) callback(code ? 'backupMySql failed with status ' + code : null);
|
||||
});
|
||||
|
||||
cp.stdout.pipe(output);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
function restoreMySql(app, callback) {
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
setupMySql(app, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'restoreMySql');
|
||||
|
||||
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
|
||||
input.on('error', callback);
|
||||
|
||||
// cannot get this to work through docker.exec
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mysql', '/addons/mysql/service.sh', 'restore', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'restoreMySql: done %s %s', code, signal);
|
||||
if (!callback.called) callback(code ? 'restoreMySql failed with status ' + code : null);
|
||||
});
|
||||
|
||||
cp.stdout.pipe(process.stdout);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
input.pipe(cp.stdin).on('error', callback);
|
||||
});
|
||||
}
|
||||
|
||||
function setupPostgreSql(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Setting up postgresql');
|
||||
|
||||
var container = docker.getContainer('postgresql');
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'add', app.id ];
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
if (error) return callback(error);
|
||||
|
||||
execContainer.start(function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var stdout = new MemoryStream();
|
||||
var stderr = new MemoryStream();
|
||||
|
||||
execContainer.modem.demuxStream(stream, stdout, stderr);
|
||||
stderr.on('data', function (data) { debugApp(app, data.toString('utf8')); }); // set -e output
|
||||
|
||||
var chunks = [ ];
|
||||
stdout.on('data', function (chunk) { chunks.push(chunk); });
|
||||
|
||||
stream.on('error', callback);
|
||||
stream.on('end', function () {
|
||||
var env = Buffer.concat(chunks).toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
debugApp(app, 'Setting postgresql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function teardownPostgreSql(app, callback) {
|
||||
var container = docker.getContainer('postgresql');
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'remove', app.id ];
|
||||
|
||||
debugApp(app, 'Tearing down postgresql');
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
if (error) return callback(error);
|
||||
|
||||
execContainer.start(function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var data = '';
|
||||
stream.on('error', callback);
|
||||
stream.on('data', function (d) { data += d.toString('utf8'); });
|
||||
stream.on('end', function () {
|
||||
appdb.unsetAddonConfig(app.id, 'postgresql', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backupPostgreSql(app, callback) {
|
||||
debugApp(app, 'Backing up postgresql');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'postgresqldump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', 'postgresql', '/addons/postgresql/service.sh', 'backup', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'backupPostgreSql: done %s %s', code, signal);
|
||||
if (!callback.called) callback(code ? 'backupPostgreSql failed with status ' + code : null);
|
||||
});
|
||||
|
||||
cp.stdout.pipe(output);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
function restorePostgreSql(app, callback) {
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
setupPostgreSql(app, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'restorePostgreSql');
|
||||
|
||||
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'postgresqldump'));
|
||||
input.on('error', callback);
|
||||
|
||||
// cannot get this to work through docker.exec
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'postgresql', '/addons/postgresql/service.sh', 'restore', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'restorePostgreSql: done %s %s', code, signal);
|
||||
if (!callback.called) callback(code ? 'restorePostgreSql failed with status ' + code : null);
|
||||
});
|
||||
|
||||
cp.stdout.pipe(process.stdout);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
input.pipe(cp.stdin).on('error', callback);
|
||||
});
|
||||
}
|
||||
|
||||
function setupMongoDb(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Setting up mongodb');
|
||||
|
||||
var container = docker.getContainer('mongodb');
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'add', app.id ];
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
if (error) return callback(error);
|
||||
|
||||
execContainer.start(function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var stdout = new MemoryStream();
|
||||
var stderr = new MemoryStream();
|
||||
|
||||
execContainer.modem.demuxStream(stream, stdout, stderr);
|
||||
stderr.on('data', function (data) { debugApp(app, data.toString('utf8')); }); // set -e output
|
||||
|
||||
var chunks = [ ];
|
||||
stdout.on('data', function (chunk) { chunks.push(chunk); });
|
||||
|
||||
stream.on('error', callback);
|
||||
stream.on('end', function () {
|
||||
var env = Buffer.concat(chunks).toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function teardownMongoDb(app, callback) {
|
||||
var container = docker.getContainer('mongodb');
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'remove', app.id ];
|
||||
|
||||
debugApp(app, 'Tearing down mongodb');
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
if (error) return callback(error);
|
||||
|
||||
execContainer.start(function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var data = '';
|
||||
stream.on('error', callback);
|
||||
stream.on('data', function (d) { data += d.toString('utf8'); });
|
||||
stream.on('end', function () {
|
||||
appdb.unsetAddonConfig(app.id, 'mongodb', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backupMongoDb(app, callback) {
|
||||
debugApp(app, 'Backing up mongodb');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mongodbdump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', 'mongodb', '/addons/mongodb/service.sh', 'backup', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'backupMongoDb: done %s %s', code, signal);
|
||||
if (!callback.called) callback(code ? 'backupMongoDb failed with status ' + code : null);
|
||||
});
|
||||
|
||||
cp.stdout.pipe(output);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
function restoreMongoDb(app, callback) {
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
setupMongoDb(app, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'restoreMongoDb');
|
||||
|
||||
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'mongodbdump'));
|
||||
input.on('error', callback);
|
||||
|
||||
// cannot get this to work through docker.exec
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mongodb', '/addons/mongodb/service.sh', 'restore', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'restoreMongoDb: done %s %s', code, signal);
|
||||
if (!callback.called) callback(code ? 'restoreMongoDb failed with status ' + code : null);
|
||||
});
|
||||
|
||||
cp.stdout.pipe(process.stdout);
|
||||
cp.stderr.pipe(process.stderr);
|
||||
input.pipe(cp.stdin).on('error', callback);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function forwardRedisPort(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.getContainer('redis-' + appId).inspect(function (error, data) {
|
||||
if (error) return callback(new Error('Unable to inspect container:' + error));
|
||||
|
||||
var redisPort = parseInt(safe.query(data, 'NetworkSettings.Ports.6379/tcp[0].HostPort'), 10);
|
||||
if (!Number.isInteger(redisPort)) return callback(new Error('Unable to get container port mapping'));
|
||||
|
||||
vbox.forwardFromHostToVirtualBox('redis-' + appId, redisPort);
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// Ensures that app's addon redis container is running. Can be called when named container already exists/running
|
||||
function setupRedis(app, callback) {
|
||||
var redisPassword = generatePassword(64, false /* memorable */);
|
||||
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
var redisDataDir = path.join(paths.DATA_DIR, app.id + '/redis');
|
||||
|
||||
if (!safe.fs.writeFileSync(redisVarsFile, 'REDIS_PASSWORD=' + redisPassword)) {
|
||||
return callback(new Error('Error writing redis config'));
|
||||
}
|
||||
|
||||
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
|
||||
|
||||
var createOptions = {
|
||||
name: 'redis-' + app.id,
|
||||
Hostname: config.appFqdn(app.location),
|
||||
Tty: true,
|
||||
Image: 'cloudron/redis:0.3.0',
|
||||
Cmd: null,
|
||||
Volumes: {},
|
||||
VolumesFrom: []
|
||||
};
|
||||
|
||||
var isMac = os.platform() === 'darwin';
|
||||
|
||||
var startOptions = {
|
||||
Binds: [
|
||||
redisVarsFile + ':/etc/redis/redis_vars.sh:r',
|
||||
redisDataDir + ':/var/lib/redis:rw'
|
||||
],
|
||||
// On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work
|
||||
// On linux, export to localhost only for testing purposes and not for the app itself
|
||||
PortBindings: {
|
||||
'6379/tcp': [{ HostPort: '0', HostIp: isMac ? '0.0.0.0' : '127.0.0.1' }]
|
||||
},
|
||||
RestartPolicy: {
|
||||
'Name': 'always',
|
||||
'MaximumRetryCount': 0
|
||||
}
|
||||
};
|
||||
|
||||
var env = [
|
||||
'REDIS_URL=redis://redisuser:' + redisPassword + '@redis-' + app.id,
|
||||
'REDIS_PASSWORD=' + redisPassword,
|
||||
'REDIS_HOST=redis-' + app.id,
|
||||
'REDIS_PORT=6379'
|
||||
];
|
||||
|
||||
var redisContainer = docker.getContainer(createOptions.name);
|
||||
redisContainer.remove({ force: true, v: false }, function (ignoredError) {
|
||||
docker.createContainer(createOptions, function (error) {
|
||||
if (error && error.statusCode !== 409) return callback(error); // if not already created
|
||||
|
||||
redisContainer.start(startOptions, function (error) {
|
||||
if (error && error.statusCode !== 304) return callback(error); // if not already running
|
||||
|
||||
appdb.setAddonConfig(app.id, 'redis', env, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
forwardRedisPort(app.id, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function teardownRedis(app, callback) {
|
||||
var container = docker.getContainer('redis-' + app.id);
|
||||
|
||||
var removeOptions = {
|
||||
force: true, // kill container if it's running
|
||||
v: false // removes volumes associated with the container
|
||||
};
|
||||
|
||||
container.remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
|
||||
|
||||
vbox.unforwardFromHostToVirtualBox('redis-' + app.id);
|
||||
|
||||
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
|
||||
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis' ], function (error, stdout, stderr) {
|
||||
if (error) return callback(new Error('Error removing redis data:' + error));
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'redis', callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function allocateAccessToken(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var expiresAt = Number.MAX_SAFE_INTEGER; // basically never expire
|
||||
var scopes = 'profile,users'; // TODO This should be put into the manifest and the user should know those
|
||||
var clientId = ''; // meaningless for apps so far
|
||||
|
||||
tokendb.delByIdentifier(tokendb.PREFIX_APP + app.id, function (error) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
tokendb.add(token, tokendb.PREFIX_APP + app.id, clientId, expiresAt, scopes, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
'CLOUDRON_TOKEN=' + token
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting token addon config to %j', env);
|
||||
|
||||
appdb.setAddonConfig(appId, 'token', env, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeAccessToken(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.delByIdentifier(tokendb.PREFIX_APP + app.id, function (error) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'token', callback);
|
||||
});
|
||||
}
|
||||
|
||||
49
src/aes-helper.js
Normal file
49
src/aes-helper.js
Normal file
@@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
|
||||
var crypto = require('crypto');
|
||||
|
||||
// This code is taken from https://github.com/fabdrol/node-aes-helper
|
||||
module.exports = {
|
||||
algorithm: 'AES-256-CBC',
|
||||
|
||||
key: function (password, salt) {
|
||||
var key = salt.toString('utf8') + password;
|
||||
var hash = crypto.createHash('sha1');
|
||||
|
||||
hash.update(key, 'utf8');
|
||||
return hash.digest('hex');
|
||||
},
|
||||
|
||||
encrypt: function (plain, password, salt) {
|
||||
var key = this.key(password, salt);
|
||||
var cipher = crypto.createCipher(this.algorithm, key);
|
||||
var crypted;
|
||||
|
||||
try {
|
||||
crypted = cipher.update(plain, 'utf8', 'hex');
|
||||
crypted += cipher.final('hex');
|
||||
} catch (e) {
|
||||
console.error('Encryption error:', e);
|
||||
crypted = '';
|
||||
}
|
||||
|
||||
return crypted;
|
||||
},
|
||||
|
||||
decrypt: function (crypted, password, salt) {
|
||||
var key = this.key(password, salt);
|
||||
var decipher = crypto.createDecipher(this.algorithm, key);
|
||||
var decoded;
|
||||
|
||||
try {
|
||||
decoded = decipher.update(crypted, 'hex', 'utf8');
|
||||
decoded += decipher.final('utf8');
|
||||
} catch (e) {
|
||||
console.error('Decryption error:', e);
|
||||
decoded = '';
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
};
|
||||
|
||||
448
src/appdb.js
Normal file
448
src/appdb.js
Normal file
@@ -0,0 +1,448 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getBySubdomain: getBySubdomain,
|
||||
getByHttpPort: getByHttpPort,
|
||||
add: add,
|
||||
exists: exists,
|
||||
del: del,
|
||||
update: update,
|
||||
getAll: getAll,
|
||||
getPortBindings: getPortBindings,
|
||||
|
||||
setAddonConfig: setAddonConfig,
|
||||
getAddonConfig: getAddonConfig,
|
||||
getAddonConfigByAppId: getAddonConfigByAppId,
|
||||
unsetAddonConfig: unsetAddonConfig,
|
||||
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
|
||||
|
||||
setHealth: setHealth,
|
||||
setInstallationCommand: setInstallationCommand,
|
||||
setRunCommand: setRunCommand,
|
||||
getAppStoreIds: getAppStoreIds,
|
||||
|
||||
// installation codes (keep in sync in UI)
|
||||
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
|
||||
ISTATE_PENDING_CONFIGURE: 'pending_configure', // config (location, port) changes and on infra update
|
||||
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
|
||||
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
|
||||
ISTATE_PENDING_UPDATE: 'pending_update', // update from installed state preserving data
|
||||
ISTATE_PENDING_FORCE_UPDATE: 'pending_force_update', // update from any state preserving data
|
||||
ISTATE_PENDING_BACKUP: 'pending_backup', // backup the app
|
||||
ISTATE_ERROR: 'error', // error executing last pending_* command
|
||||
ISTATE_INSTALLED: 'installed', // app is installed
|
||||
|
||||
// run codes (keep in sync in UI)
|
||||
RSTATE_RUNNING: 'running',
|
||||
RSTATE_PENDING_START: 'pending_start',
|
||||
RSTATE_PENDING_STOP: 'pending_stop',
|
||||
RSTATE_STOPPED: 'stopped', // app stopped by use
|
||||
|
||||
HEALTH_HEALTHY: 'healthy',
|
||||
HEALTH_UNHEALTHY: 'unhealthy',
|
||||
HEALTH_ERROR: 'error',
|
||||
HEALTH_DEAD: 'dead',
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var APPS_FIELDS = [ 'id', 'appStoreId', 'installationState', 'installationProgress', 'runState',
|
||||
'health', 'containerId', 'manifestJson', 'httpPort', 'location', 'dnsRecordId',
|
||||
'accessRestriction', 'lastBackupId', 'lastBackupConfigJson', 'oldConfigJson' ].join(',');
|
||||
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
|
||||
'apps.accessRestriction', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
function postProcess(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
|
||||
assert(result.manifestJson === null || typeof result.manifestJson === 'string');
|
||||
result.manifest = safe.JSON.parse(result.manifestJson);
|
||||
delete result.manifestJson;
|
||||
|
||||
assert(result.lastBackupConfigJson === null || typeof result.lastBackupConfigJson === 'string');
|
||||
result.lastBackupConfig = safe.JSON.parse(result.lastBackupConfigJson);
|
||||
delete result.lastBackupConfigJson;
|
||||
|
||||
assert(result.oldConfigJson === null || typeof result.oldConfigJson === 'string');
|
||||
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
|
||||
delete result.oldConfigJson;
|
||||
|
||||
assert(result.hostPorts === null || typeof result.hostPorts === 'string');
|
||||
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
|
||||
|
||||
result.portBindings = { };
|
||||
var hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
|
||||
var environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
|
||||
|
||||
delete result.hostPorts;
|
||||
delete result.environmentVariables;
|
||||
|
||||
for (var i = 0; i < environmentVariables.length; i++) {
|
||||
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE apps.id = ? GROUP BY apps.id', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getBySubdomain(subdomain, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE location = ? GROUP BY apps.id', [ subdomain ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getByHttpPort(httpPort, callback) {
|
||||
assert.strictEqual(typeof httpPort, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE httpPort = ? GROUP BY apps.id', [ httpPort ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' GROUP BY apps.id ORDER BY apps.id', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(postProcess);
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof manifest.version, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof accessRestriction, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
portBindings = portBindings || { };
|
||||
|
||||
var manifestJson = JSON.stringify(manifest);
|
||||
|
||||
var queries = [ ];
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestriction) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestriction ]
|
||||
});
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, appId) VALUES (?, ?, ?)',
|
||||
args: [ env, portBindings[env], id ]
|
||||
});
|
||||
});
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function exists(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT 1 FROM apps WHERE id=?', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result.length !== 0);
|
||||
});
|
||||
}
|
||||
|
||||
function getPortBindings(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + PORT_BINDINGS_FIELDS + ' FROM appPortBindings WHERE appId = ?', [ id ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
var portBindings = { };
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
portBindings[results[i].environmentVariable] = results[i].hostPort;
|
||||
}
|
||||
|
||||
callback(null, portBindings);
|
||||
});
|
||||
}
|
||||
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var queries = [
|
||||
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
database.query.bind(null, 'DELETE FROM appPortBindings'),
|
||||
database.query.bind(null, 'DELETE FROM appAddonConfigs'),
|
||||
database.query.bind(null, 'DELETE FROM apps')
|
||||
], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function update(id, app, callback) {
|
||||
updateWithConstraints(id, app, '', callback);
|
||||
}
|
||||
|
||||
function updateWithConstraints(id, app, constraints, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof constraints, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
|
||||
|
||||
var queries = [ ];
|
||||
|
||||
if ('portBindings' in app) {
|
||||
var portBindings = app.portBindings || { };
|
||||
// replace entries by app id
|
||||
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
var values = [ portBindings[env], env, id ];
|
||||
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, environmentVariable, appId) VALUES(?, ?, ?)', args: values });
|
||||
});
|
||||
}
|
||||
|
||||
var fields = [ ], values = [ ];
|
||||
for (var p in app) {
|
||||
if (p === 'manifest') {
|
||||
fields.push('manifestJson = ?');
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p === 'lastBackupConfig') {
|
||||
fields.push('lastBackupConfigJson = ?');
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p === 'oldConfig') {
|
||||
fields.push('oldConfigJson = ?');
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings') {
|
||||
fields.push(p + ' = ?');
|
||||
values.push(app[p]);
|
||||
}
|
||||
}
|
||||
|
||||
if (values.length !== 0) {
|
||||
values.push(id);
|
||||
queries.push({ query: 'UPDATE apps SET ' + fields.join(', ') + ' WHERE id = ? ' + constraints, args: values });
|
||||
}
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results[results.length - 1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// not sure if health should influence runState
|
||||
function setHealth(appId, health, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof health, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var values = { health: health };
|
||||
|
||||
var constraints = 'AND runState NOT LIKE "pending_%" AND installationState = "installed"';
|
||||
|
||||
updateWithConstraints(appId, values, constraints, callback);
|
||||
}
|
||||
|
||||
function setInstallationCommand(appId, installationState, values, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof installationState, 'string');
|
||||
|
||||
if (typeof values === 'function') {
|
||||
callback = values;
|
||||
values = { };
|
||||
} else {
|
||||
assert.strictEqual(typeof values, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
}
|
||||
|
||||
values.installationState = installationState;
|
||||
values.installationProgress = '';
|
||||
|
||||
// Rules are:
|
||||
// uninstall is allowed in any state
|
||||
// restore is allowed from installed or error state
|
||||
// update and configure are allowed only in installed state
|
||||
|
||||
if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) {
|
||||
updateWithConstraints(appId, values, '', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error")', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_UPDATE || exports.ISTATE_PENDING_CONFIGURE || installationState == exports.ISTATE_PENDING_BACKUP) {
|
||||
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
|
||||
} else {
|
||||
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
|
||||
}
|
||||
}
|
||||
|
||||
function setRunCommand(appId, runState, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof runState, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var values = { runState: runState };
|
||||
updateWithConstraints(appId, values, 'AND runState NOT LIKE "pending_%" AND installationState = "installed"', callback);
|
||||
}
|
||||
|
||||
function getAppStoreIds(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT id, appStoreId FROM apps', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function setAddonConfig(appId, addonId, env, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert(util.isArray(env));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
unsetAddonConfig(appId, addonId, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (env.length === 0) return callback(null);
|
||||
|
||||
var query = 'INSERT INTO appAddonConfigs(appId, addonId, value) VALUES ';
|
||||
var args = [ ], queryArgs = [ ];
|
||||
for (var i = 0; i < env.length; i++) {
|
||||
args.push(appId, addonId, env[i]);
|
||||
queryArgs.push('(?, ?, ?)');
|
||||
}
|
||||
|
||||
database.query(query + queryArgs.join(','), args, function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function unsetAddonConfig(appId, addonId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function unsetAddonConfigByAppId(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getAddonConfig(appId, addonId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
var config = [ ];
|
||||
results.forEach(function (v) { config.push(v.value); });
|
||||
|
||||
callback(null, config);
|
||||
});
|
||||
}
|
||||
|
||||
function getAddonConfigByAppId(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
var config = [ ];
|
||||
results.forEach(function (v) { config.push(v.value); });
|
||||
|
||||
callback(null, config);
|
||||
});
|
||||
}
|
||||
|
||||
765
src/apps.js
Normal file
765
src/apps.js
Normal file
@@ -0,0 +1,765 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
AppsError: AppsError,
|
||||
|
||||
get: get,
|
||||
getBySubdomain: getBySubdomain,
|
||||
getAll: getAll,
|
||||
purchase: purchase,
|
||||
install: install,
|
||||
configure: configure,
|
||||
uninstall: uninstall,
|
||||
|
||||
restore: restore,
|
||||
restoreApp: restoreApp,
|
||||
|
||||
update: update,
|
||||
|
||||
backup: backup,
|
||||
backupApp: backupApp,
|
||||
|
||||
getLogStream: getLogStream,
|
||||
getLogs: getLogs,
|
||||
|
||||
start: start,
|
||||
stop: stop,
|
||||
|
||||
exec: exec,
|
||||
|
||||
checkManifestConstraints: checkManifestConstraints,
|
||||
|
||||
setRestorePoint: setRestorePoint,
|
||||
|
||||
autoupdateApps: autoupdateApps,
|
||||
|
||||
// exported for testing
|
||||
_validateHostname: validateHostname,
|
||||
_validatePortBindings: validatePortBindings
|
||||
};
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:apps'),
|
||||
docker = require('./docker.js'),
|
||||
fs = require('fs'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
shell = require('./shell.js'),
|
||||
split = require('split'),
|
||||
superagent = require('superagent'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
util = require('util'),
|
||||
validator = require('validator');
|
||||
|
||||
var BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
|
||||
RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'),
|
||||
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh');
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
var prefix = app ? app.location : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function ignoreError(func) {
|
||||
return function (callback) {
|
||||
func(function (error) {
|
||||
if (error) console.error('Ignored error:', error);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// http://dustinsenos.com/articles/customErrorsInNode
|
||||
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
|
||||
function AppsError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(AppsError, Error);
|
||||
AppsError.INTERNAL_ERROR = 'Internal Error';
|
||||
AppsError.EXTERNAL_ERROR = 'External Error';
|
||||
AppsError.ALREADY_EXISTS = 'Already Exists';
|
||||
AppsError.NOT_FOUND = 'Not Found';
|
||||
AppsError.BAD_FIELD = 'Bad Field';
|
||||
AppsError.BAD_STATE = 'Bad State';
|
||||
AppsError.PORT_RESERVED = 'Port Reserved';
|
||||
AppsError.PORT_CONFLICT = 'Port Conflict';
|
||||
AppsError.BILLING_REQUIRED = 'Billing Required';
|
||||
|
||||
// Hostname validation comes from RFC 1123 (section 2.1)
|
||||
// Domain name validation comes from RFC 2181 (Name syntax)
|
||||
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||
// We are validating the validity of the location-fqdn as host name
|
||||
function validateHostname(location, fqdn) {
|
||||
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION ];
|
||||
|
||||
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new Error(location + ' is reserved');
|
||||
|
||||
if (location === '') return null; // bare location
|
||||
|
||||
if ((location.length + 1 /*+ hyphen */ + fqdn.indexOf('.')) > 63) return new Error('Hostname length cannot be greater than 63');
|
||||
if (location.match(/^[A-Za-z0-9-]+$/) === null) return new Error('Hostname can only contain alphanumerics and hyphen');
|
||||
if (location[0] === '-' || location[location.length-1] === '-') return new Error('Hostname cannot start or end with hyphen');
|
||||
if (location.length + 1 /* hyphen */ + fqdn.length > 253) return new Error('FQDN length exceeds 253 characters');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// validate the port bindings
|
||||
function validatePortBindings(portBindings, tcpPorts) {
|
||||
// keep the public ports in sync with firewall rules in scripts/initializeBaseUbuntuImage.sh
|
||||
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
|
||||
// for custom tcp ports
|
||||
var RESERVED_PORTS = [
|
||||
22, /* ssh */
|
||||
25, /* smtp */
|
||||
53, /* dns */
|
||||
80, /* http */
|
||||
443, /* https */
|
||||
2003, /* graphite (lo) */
|
||||
2004, /* graphite (lo) */
|
||||
2020, /* install server */
|
||||
config.get('port'), /* app server (lo) */
|
||||
config.get('internalPort'), /* internal app server (lo) */
|
||||
3306, /* mysql (lo) */
|
||||
8000 /* graphite (lo) */
|
||||
];
|
||||
|
||||
if (!portBindings) return null;
|
||||
|
||||
var env;
|
||||
for (env in portBindings) {
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(env)) return new AppsError(AppsError.BAD_FIELD, env + ' is not valid environment variable');
|
||||
|
||||
if (!Number.isInteger(portBindings[env])) return new Error(portBindings[env] + ' is not an integer');
|
||||
if (portBindings[env] <= 0 || portBindings[env] > 65535) return new Error(portBindings[env] + ' is out of range');
|
||||
|
||||
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, + portBindings[env]);
|
||||
}
|
||||
|
||||
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
|
||||
// that the user wants the service disabled
|
||||
tcpPorts = tcpPorts || { };
|
||||
for (env in portBindings) {
|
||||
if (!(env in tcpPorts)) return new AppsError(AppsError.BAD_FIELD, 'Invalid portBindings ' + env);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS);
|
||||
|
||||
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key/);
|
||||
if (!match) {
|
||||
console.error('Unexpected SQL error message.', error);
|
||||
return new AppsError(AppsError.INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
// check if the location conflicts
|
||||
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
|
||||
|
||||
// check if any of the port bindings conflict
|
||||
for (var env in portBindings) {
|
||||
if (portBindings[env] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
|
||||
}
|
||||
|
||||
return new AppsError(AppsError.ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
function getIconUrlSync(app) {
|
||||
var iconPath = paths.APPICONS_DIR + '/' + app.id + '.png';
|
||||
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
|
||||
}
|
||||
|
||||
function get(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = config.appFqdn(app.location);
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
}
|
||||
|
||||
function getBySubdomain(subdomain, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.getBySubdomain(subdomain, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = config.appFqdn(app.location);
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
apps.forEach(function (app) {
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = config.appFqdn(app.location);
|
||||
});
|
||||
|
||||
callback(null, apps);
|
||||
});
|
||||
}
|
||||
|
||||
function validateAccessRestriction(accessRestriction) {
|
||||
// TODO: make the values below enumerations in the oauth code
|
||||
switch (accessRestriction) {
|
||||
case '':
|
||||
case 'roleUser':
|
||||
case 'roleAdmin':
|
||||
return null;
|
||||
default:
|
||||
return new Error('Invalid accessRestriction');
|
||||
}
|
||||
}
|
||||
|
||||
function purchase(appStoreId, callback) {
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// Skip purchase if appStoreId is empty
|
||||
if (appStoreId === '') return callback(null);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/apps/' + appStoreId + '/purchase';
|
||||
|
||||
superagent.post(url).query({ token: config.token() }).end(function (error, res) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
if (res.status === 402) return callback(new AppsError(AppsError.BILLING_REQUIRED));
|
||||
if (res.status !== 201 && res.status !== 200) return callback(new Error(util.format('App purchase failed. %s %j', res.status, res.body)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof accessRestriction, 'string');
|
||||
assert(!icon || typeof icon === 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = manifestFormat.parse(manifest);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error: ' + error.message));
|
||||
|
||||
error = checkManifestConstraints(manifest);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
|
||||
|
||||
error = validateHostname(location, config.fqdn());
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
error = validatePortBindings(portBindings, manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateAccessRestriction(accessRestriction);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
if (icon) {
|
||||
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
|
||||
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
|
||||
}
|
||||
}
|
||||
|
||||
debug('Will install app with id : ' + appId);
|
||||
|
||||
purchase(appStoreId, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function configure(appId, location, portBindings, accessRestriction, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof accessRestriction, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateHostname(location, config.fqdn());
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
error = validateAccessRestriction(accessRestriction);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
error = validatePortBindings(portBindings, app.manifest.tcpPorts);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
var values = {
|
||||
location: location.toLowerCase(),
|
||||
accessRestriction: accessRestriction,
|
||||
portBindings: portBindings,
|
||||
|
||||
oldConfig: {
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings
|
||||
}
|
||||
};
|
||||
|
||||
debug('Will configure app with id:%s values:%j', appId, values);
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function update(appId, force, manifest, portBindings, icon, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof force, 'boolean');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert(!portBindings || typeof portBindings === 'object');
|
||||
assert(!icon || typeof icon === 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Will update app with id:%s', appId);
|
||||
|
||||
var error = manifestFormat.parse(manifest);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
|
||||
|
||||
error = checkManifestConstraints(manifest);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed:' + error.message));
|
||||
|
||||
error = validatePortBindings(portBindings, manifest.tcpPorts);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
if (icon) {
|
||||
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
|
||||
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
|
||||
}
|
||||
}
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var values = {
|
||||
manifest: manifest,
|
||||
portBindings: portBindings,
|
||||
oldConfig: {
|
||||
manifest: app.manifest,
|
||||
portBindings: app.portBindings
|
||||
}
|
||||
};
|
||||
|
||||
appdb.setInstallationCommand(appId, force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, values, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails('' /* location cannot conflict */, portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getLogStream(appId, fromLine, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof fromLine, 'number'); // behaves like tail -n
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Getting logs for %s', appId);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState)));
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
var tail = fromLine < 0 ? -fromLine : 'all';
|
||||
|
||||
// note: cannot access docker file directly because it needs root access
|
||||
container.logs({ stdout: true, stderr: true, follow: true, timestamps: true, tail: tail }, function (error, logStream) {
|
||||
if (error && error.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var lineCount = 0;
|
||||
var skipLinesStream = split(function mapper(line) {
|
||||
if (++lineCount < fromLine) return undefined;
|
||||
var timestamp = line.substr(0, line.indexOf(' ')); // sometimes this has square brackets around it
|
||||
return JSON.stringify({ lineNumber: lineCount, timestamp: timestamp.replace(/[[\]]/g,''), log: line.substr(timestamp.length + 1) });
|
||||
});
|
||||
skipLinesStream.close = logStream.req.abort;
|
||||
logStream.pipe(skipLinesStream);
|
||||
return callback(null, skipLinesStream);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getLogs(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Getting logs for %s', appId);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState)));
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
// note: cannot access docker file directly because it needs root access
|
||||
container.logs({ stdout: true, stderr: true, follow: false, timestamps: true, tail: 'all' }, function (error, logStream) {
|
||||
if (error && error.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, logStream);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function restore(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Will restore app with id:%s', appId);
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var restoreConfig = app.lastBackupConfig;
|
||||
if (!restoreConfig) return callback(new AppsError(AppsError.BAD_STATE, 'No restore point'));
|
||||
|
||||
// re-validate because this new box version may not accept old configs. if we restore location, it should be validated here as well
|
||||
error = checkManifestConstraints(restoreConfig.manifest);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
|
||||
|
||||
error = validatePortBindings(restoreConfig.portBindings, restoreConfig.manifest.tcpPorts); // maybe new ports got reserved now
|
||||
if (error) return callback(error);
|
||||
|
||||
// ## should probably query new location, access restriction from user
|
||||
var values = {
|
||||
manifest: restoreConfig.manifest,
|
||||
portBindings: restoreConfig.portBindings,
|
||||
|
||||
oldConfig: {
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
manifest: app.manifest
|
||||
}
|
||||
};
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function uninstall(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Will uninstall app with id:%s', appId);
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId); // since uninstall is allowed from any state, kill current task
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function start(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Will start app with id:%s', appId);
|
||||
|
||||
appdb.setRunCommand(appId, appdb.RSTATE_PENDING_START, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function stop(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Will stop app with id:%s', appId);
|
||||
|
||||
appdb.setRunCommand(appId, appdb.RSTATE_PENDING_STOP, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function checkManifestConstraints(manifest) {
|
||||
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
|
||||
return new Error('Box version exceeds Apps maxBoxVersion');
|
||||
}
|
||||
|
||||
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.version())) {
|
||||
return new Error('minBoxVersion exceeds Box version');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function exec(appId, options, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert(options && typeof options === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var cmd = options.cmd || [ '/bin/bash' ];
|
||||
assert(util.isArray(cmd) && cmd.length > 0);
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
|
||||
var execOptions = {
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
Cmd: cmd
|
||||
};
|
||||
|
||||
container.exec(execOptions, function (error, exec) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
var startOptions = {
|
||||
Detach: false,
|
||||
Tty: true,
|
||||
stdin: true // this is a dockerode option that enabled openStdin in the modem
|
||||
};
|
||||
exec.start(startOptions, function(error, stream) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (options.rows && options.columns) {
|
||||
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
|
||||
}
|
||||
|
||||
return callback(null, stream);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setRestorePoint(appId, lastBackupId, lastBackupConfig, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof lastBackupId, 'string');
|
||||
assert.strictEqual(typeof lastBackupConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.update(appId, { lastBackupId: lastBackupId, lastBackupConfig: lastBackupConfig }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { manifest } }
|
||||
assert.strictEqual(typeof updateInfo, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function canAutoupdateApp(app, newManifest) {
|
||||
// TODO: maybe check the description as well?
|
||||
if (!newManifest.tcpPorts && !app.portBindings) return true;
|
||||
if (!newManifest.tcpPorts || !app.portBindings) return false;
|
||||
|
||||
for (var env in newManifest.tcpPorts) {
|
||||
if (!(env in app.portBindings)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!updateInfo) return callback(null);
|
||||
|
||||
async.eachSeries(Object.keys(updateInfo), function iterator(appId, iteratorDone) {
|
||||
get(appId, function (error, app) {
|
||||
if (!canAutoupdateApp(app, updateInfo.manifest)) {
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
update(appId, updateInfo.manifest, app.portBindings, null /* icon */, function (error) {
|
||||
if (error) debug('Error initiating autoupdate of %s', appId);
|
||||
|
||||
iteratorDone(null);
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function backupApp(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function canBackupApp(app) {
|
||||
// only backup apps that are installed or pending configure. Rest of them are in some
|
||||
// state not good for consistent backup
|
||||
|
||||
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY)
|
||||
|| app.installationState === appdb.ISTATE_PENDING_CONFIGURE
|
||||
|| app.installationState === appdb.ISTATE_PENDING_BACKUP
|
||||
|| app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
|
||||
}
|
||||
|
||||
if (!canBackupApp(app)) return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy'));
|
||||
|
||||
var appConfig = {
|
||||
manifest: app.manifest,
|
||||
location: app.location,
|
||||
portBindings: app.portBindings,
|
||||
accessRestriction: app.accessRestriction
|
||||
};
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
backups.getBackupUrl(app, null, function (error, result) {
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
debugApp(app, 'backupApp: backup url:%s backup id:%s', result.url, result.id);
|
||||
|
||||
async.series([
|
||||
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
|
||||
addons.backupAddons.bind(null, app, app.manifest.addons),
|
||||
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, result.url, result.backupKey ]),
|
||||
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
|
||||
], function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
debugApp(app, 'backupApp: successful id:%s', result.id);
|
||||
|
||||
setRestorePoint(app.id, result.id, appConfig, function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backup(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_BACKUP, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function restoreApp(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(app.lastBackupId);
|
||||
|
||||
backups.getRestoreUrl(app.lastBackupId, function (error, result) {
|
||||
if (error && error.reason == BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
debugApp(app, 'restoreApp: restoreUrl:%s', result.url);
|
||||
|
||||
shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey ], function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
addons.restoreAddons(app, app.manifest.addons, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
929
src/apptask.js
Normal file
929
src/apptask.js
Normal file
@@ -0,0 +1,929 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
startTask: startTask,
|
||||
|
||||
// exported for testing
|
||||
_getFreePort: getFreePort,
|
||||
_configureNginx: configureNginx,
|
||||
_unconfigureNginx: unconfigureNginx,
|
||||
_createVolume: createVolume,
|
||||
_deleteVolume: deleteVolume,
|
||||
_allocateOAuthProxyCredentials: allocateOAuthProxyCredentials,
|
||||
_removeOAuthProxyCredentials: removeOAuthProxyCredentials,
|
||||
_verifyManifest: verifyManifest,
|
||||
_registerSubdomain: registerSubdomain,
|
||||
_unregisterSubdomain: unregisterSubdomain,
|
||||
_reloadNginx: reloadNginx,
|
||||
_waitForDnsPropagation: waitForDnsPropagation
|
||||
};
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
config = require('./config.js'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:apptask'),
|
||||
docker = require('./docker.js'),
|
||||
ejs = require('ejs'),
|
||||
fs = require('fs'),
|
||||
hat = require('hat'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
net = require('net'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid'),
|
||||
vbox = require('./vbox.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
|
||||
COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
|
||||
RELOAD_COLLECTD_CMD = path.join(__dirname, 'scripts/reloadcollectd.sh'),
|
||||
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
|
||||
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
|
||||
|
||||
function initialize(callback) {
|
||||
database.initialize(callback);
|
||||
}
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
var prefix = app ? (app.location || '(bare)') : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
// We expect conflicts to not happen despite closing the port (parallel app installs, app update does not reconfigure nginx etc)
|
||||
// https://tools.ietf.org/html/rfc6056#section-3.5 says linux uses random ephemeral port allocation
|
||||
function getFreePort(callback) {
|
||||
var server = net.createServer();
|
||||
server.listen(0, function () {
|
||||
var port = server.address().port;
|
||||
server.close(function () {
|
||||
return callback(null, port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reloadNginx(callback) {
|
||||
shell.sudo('reloadNginx', [ RELOAD_NGINX_CMD ], callback);
|
||||
}
|
||||
|
||||
function configureNginx(app, callback) {
|
||||
getFreePort(function (error, freePort) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var sourceDir = path.resolve(__dirname, '..');
|
||||
var endpoint = app.accessRestriction ? 'oauthproxy' : 'app';
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, vhost: config.appFqdn(app.location), port: freePort, endpoint: endpoint });
|
||||
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
debugApp(app, 'writing config to %s', nginxConfigFilename);
|
||||
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
|
||||
debugApp(app, 'Error creating nginx config : %s', safe.error.message);
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
async.series([
|
||||
exports._reloadNginx,
|
||||
updateApp.bind(null, app, { httpPort: freePort })
|
||||
], callback);
|
||||
|
||||
vbox.forwardFromHostToVirtualBox(app.id + '-http', freePort);
|
||||
});
|
||||
}
|
||||
|
||||
function unconfigureNginx(app, callback) {
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
|
||||
debugApp(app, 'Error removing nginx configuration : %s', safe.error.message);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
exports._reloadNginx(callback);
|
||||
|
||||
vbox.unforwardFromHostToVirtualBox(app.id + '-http');
|
||||
}
|
||||
|
||||
function downloadImage(app, callback) {
|
||||
debugApp(app, 'downloadImage %s', app.manifest.dockerImage);
|
||||
|
||||
docker.pull(app.manifest.dockerImage, function (err, stream) {
|
||||
if (err) return callback(new Error('Error connecting to docker'));
|
||||
|
||||
// https://github.com/dotcloud/docker/issues/1074 says each status message
|
||||
// is emitted as a chunk
|
||||
stream.on('data', function (chunk) {
|
||||
var data = safe.JSON.parse(chunk) || { };
|
||||
debugApp(app, 'downloadImage data: %j', data);
|
||||
|
||||
// The information here is useless because this is per layer as opposed to per image
|
||||
if (data.status) {
|
||||
debugApp(app, 'progress: %s', data.status); // progressDetail { current, total }
|
||||
} else if (data.error) {
|
||||
debugApp(app, 'error detail: %s', data.errorDetail.message);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', function () {
|
||||
debugApp(app, 'download image successfully');
|
||||
|
||||
var image = docker.getImage(app.manifest.dockerImage);
|
||||
|
||||
image.inspect(function (err, data) {
|
||||
if (err) {
|
||||
return callback(new Error('Error inspecting image:' + err.message));
|
||||
}
|
||||
|
||||
if (!data || !data.Config) {
|
||||
return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
|
||||
}
|
||||
|
||||
if (!data.Config.Entrypoint && !data.Config.Cmd) {
|
||||
return callback(new Error('Only images with entry point are allowed'));
|
||||
}
|
||||
|
||||
debugApp(app, 'This image exposes ports: %j', data.Config.ExposedPorts);
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createContainer(app, callback) {
|
||||
appdb.getPortBindings(app.id, function (error, portBindings) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var manifest = app.manifest;
|
||||
var exposedPorts = {};
|
||||
var env = [];
|
||||
|
||||
// docker portBindings requires ports to be exposed
|
||||
exposedPorts[manifest.httpPort + '/tcp'] = {};
|
||||
|
||||
for (var e in portBindings) {
|
||||
var hostPort = portBindings[e];
|
||||
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
|
||||
exposedPorts[containerPort + '/tcp'] = {};
|
||||
|
||||
env.push(e + '=' + hostPort);
|
||||
}
|
||||
|
||||
env.push('CLOUDRON=1');
|
||||
env.push('ADMIN_ORIGIN' + '=' + config.adminOrigin()); // ## remove
|
||||
env.push('WEBADMIN_ORIGIN' + '=' + config.adminOrigin());
|
||||
env.push('API_ORIGIN' + '=' + config.adminOrigin());
|
||||
|
||||
addons.getEnvironment(app, function (error, addonEnv) {
|
||||
if (error) return callback(new Error('Error getting addon env: ' + error));
|
||||
|
||||
var containerOptions = {
|
||||
name: app.id,
|
||||
Hostname: config.appFqdn(app.location),
|
||||
Tty: true,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: null,
|
||||
Volumes: {},
|
||||
VolumesFrom: [],
|
||||
Env: env.concat(addonEnv),
|
||||
ExposedPorts: exposedPorts
|
||||
};
|
||||
|
||||
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
|
||||
|
||||
docker.createContainer(containerOptions, function (error, container) {
|
||||
if (error) return callback(new Error('Error creating container: ' + error));
|
||||
|
||||
updateApp(app, { containerId: container.id }, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteContainer(app, callback) {
|
||||
if (app.containerId === null) return callback(null);
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
|
||||
var removeOptions = {
|
||||
force: true, // kill container if it's running
|
||||
v: false // removes volumes associated with the container
|
||||
};
|
||||
|
||||
container.remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode === 404) return updateApp(app, { containerId: null }, callback);
|
||||
|
||||
if (error) debugApp(app, 'Error removing container', error);
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteImage(app, manifest, callback) {
|
||||
var dockerImage = manifest ? manifest.dockerImage : null;
|
||||
if (!dockerImage) return callback(null);
|
||||
|
||||
docker.getImage(dockerImage).inspect(function (error, result) {
|
||||
if (error && error.statusCode === 404) return callback(null);
|
||||
|
||||
if (error) return callback(error);
|
||||
|
||||
var removeOptions = {
|
||||
force: true,
|
||||
noprune: false
|
||||
};
|
||||
|
||||
// delete image by id because docker pull pulls down all the tags and this is the only way to delete all tags
|
||||
docker.getImage(result.Id).remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode === 404) return callback(null);
|
||||
if (error && error.statusCode === 409) return callback(null); // another container using the image
|
||||
|
||||
if (error) debugApp(app, 'Error removing image', error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createVolume(app, callback) {
|
||||
shell.sudo('createVolume', [ CREATEAPPDIR_CMD, app.id ], callback);
|
||||
}
|
||||
|
||||
function deleteVolume(app, callback) {
|
||||
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id ], callback);
|
||||
}
|
||||
|
||||
function allocateOAuthProxyCredentials(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!app.accessRestriction) return callback(null);
|
||||
|
||||
var appId = 'proxy-' + app.id;
|
||||
var id = 'cid-proxy-' + uuid.v4();
|
||||
var clientSecret = hat(256);
|
||||
var redirectURI = 'https://' + config.appFqdn(app.location);
|
||||
var scope = 'profile,' + app.accessRestriction;
|
||||
|
||||
clientdb.add(id, appId, clientSecret, redirectURI, scope, callback);
|
||||
}
|
||||
|
||||
function removeOAuthProxyCredentials(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.delByAppId('proxy-' + app.id, function (error) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) {
|
||||
debugApp(app, 'Error removing OAuth client id', error);
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function addCollectdProfile(app, callback) {
|
||||
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId });
|
||||
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
|
||||
if (error) return callback(error);
|
||||
shell.sudo('addCollectdProfile', [ RELOAD_COLLECTD_CMD ], callback);
|
||||
});
|
||||
}
|
||||
|
||||
function removeCollectdProfile(app, callback) {
|
||||
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) {
|
||||
if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error);
|
||||
shell.sudo('removeCollectdProfile', [ RELOAD_COLLECTD_CMD ], callback);
|
||||
});
|
||||
}
|
||||
|
||||
function startContainer(app, callback) {
|
||||
appdb.getPortBindings(app.id, function (error, portBindings) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var manifest = app.manifest;
|
||||
|
||||
var dockerPortBindings = { };
|
||||
var isMac = os.platform() === 'darwin';
|
||||
|
||||
// On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work
|
||||
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: isMac ? '0.0.0.0' : '127.0.0.1', HostPort: app.httpPort + '' } ];
|
||||
|
||||
for (var env in portBindings) {
|
||||
var hostPort = portBindings[env];
|
||||
var containerPort = manifest.tcpPorts[env].containerPort || hostPort;
|
||||
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
||||
vbox.forwardFromHostToVirtualBox(app.id + '-tcp' + containerPort, hostPort);
|
||||
}
|
||||
|
||||
var startOptions = {
|
||||
Binds: addons.getBindsSync(app, app.manifest.addons),
|
||||
PortBindings: dockerPortBindings,
|
||||
PublishAllPorts: false,
|
||||
Links: addons.getLinksSync(app, app.manifest.addons),
|
||||
RestartPolicy: {
|
||||
"Name": "always",
|
||||
"MaximumRetryCount": 0
|
||||
}
|
||||
};
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
debugApp(app, 'Starting container %s with options: %j', container.id, JSON.stringify(startOptions));
|
||||
|
||||
container.start(startOptions, function (error, data) {
|
||||
if (error && error.statusCode !== 304) return callback(new Error('Error starting container:' + error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function stopContainer(app, callback) {
|
||||
var container = docker.getContainer(app.containerId);
|
||||
debugApp(app, 'Stopping container %s', container.id);
|
||||
|
||||
var options = {
|
||||
t: 10 // wait for 10 seconds before killing it
|
||||
};
|
||||
|
||||
container.stop(options, function (error) {
|
||||
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error stopping container:' + error));
|
||||
|
||||
var tcpPorts = safe.query(app, 'manifest.tcpPorts', { });
|
||||
for (var containerPort in tcpPorts) {
|
||||
vbox.unforwardFromHostToVirtualBox(app.id + '-tcp' + containerPort);
|
||||
}
|
||||
|
||||
debugApp(app, 'Waiting for container ' + container.id);
|
||||
|
||||
container.wait(function (error, data) {
|
||||
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error waiting on container:' + error));
|
||||
|
||||
debugApp(app, 'Container stopped with status code [%s]', data ? String(data.StatusCode) : '');
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verifyManifest(app, callback) {
|
||||
debugApp(app, 'Verifying manifest');
|
||||
|
||||
var manifest = app.manifest;
|
||||
var error = manifestFormat.parse(manifest);
|
||||
if (error) return callback(new Error(util.format('Manifest error: %s', error.message)));
|
||||
|
||||
error = apps.checkManifestConstraints(manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function downloadIcon(app, callback) {
|
||||
debugApp(app, 'Downloading icon of %s@%s', app.appStoreId, app.manifest.version);
|
||||
|
||||
var iconUrl = config.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
|
||||
|
||||
superagent
|
||||
.get(iconUrl)
|
||||
.buffer(true)
|
||||
.end(function (error, res) {
|
||||
if (error) return callback(new Error('Error downloading icon:' + error.message));
|
||||
if (res.status !== 200) return callback(null); // ignore error. this can also happen for apps installed with cloudron-cli
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, app.id + '.png'), res.body)) return callback(new Error('Error saving icon:' + safe.error.message));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function registerSubdomain(app, callback) {
|
||||
debugApp(app, 'Registering subdomain location [%s]', app.location);
|
||||
|
||||
// even though the bare domain is already registered in the appstore, we still
|
||||
// need to register it so that we have a dnsRecordId to wait for it to complete
|
||||
var record = { subdomain: app.location, type: 'A', value: sysinfo.getIp() };
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/subdomains')
|
||||
.set('Accept', 'application/json')
|
||||
.query({ token: config.token() })
|
||||
.send({ records: [ record ] })
|
||||
.end(function (error, res) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'Registered subdomain status: %s', res.status);
|
||||
|
||||
if (res.status === 409) return callback(null); // already registered
|
||||
if (res.status !== 201) return callback(new Error(util.format('Subdomain Registration failed. %s %j', res.status, res.body)));
|
||||
|
||||
updateApp(app, { dnsRecordId: res.body.ids[0] }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterSubdomain(app, callback) {
|
||||
debugApp(app, 'Unregistering subdomain: dnsRecordId=%s', app.dnsRecordId);
|
||||
|
||||
if (!app.dnsRecordId) return callback(null);
|
||||
|
||||
// do not unregister bare domain because we show a error/cloudron info page there
|
||||
if (app.location === '') return updateApp(app, { dnsRecordId: null }, callback);
|
||||
|
||||
superagent
|
||||
.del(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId)
|
||||
.query({ token: config.token() })
|
||||
.end(function (error, res) {
|
||||
if (error) {
|
||||
debugApp(app, 'Error making request: %s', error);
|
||||
} else if (res.status !== 204) {
|
||||
debugApp(app, 'Error unregistering subdomain:', res.status, res.body);
|
||||
}
|
||||
|
||||
updateApp(app, { dnsRecordId: null }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function removeIcon(app, callback) {
|
||||
fs.unlink(path.join(paths.APPICONS_DIR, app.id + '.png'), function (error) {
|
||||
if (error && error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', error);
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForDnsPropagation(app, callback) {
|
||||
if (!config.CLOUDRON) {
|
||||
debugApp(app, 'Skipping dns propagation check for development');
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function retry(error) {
|
||||
debugApp(app, 'waitForDnsPropagation: ', error);
|
||||
setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000);
|
||||
}
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId + '/status')
|
||||
.set('Accept', 'application/json')
|
||||
.query({ token: config.token() })
|
||||
.end(function (error, res) {
|
||||
if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
|
||||
|
||||
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, res.status);
|
||||
|
||||
if (res.status !== 200) return retry(new Error(util.format('Error getting record status: %s %j', res.status, res.body)));
|
||||
|
||||
if (res.body.status !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, res.body.status)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// updates the app object and the database
|
||||
function updateApp(app, values, callback) {
|
||||
debugApp(app, 'installationState: %s progress: %s', app.installationState, app.installationProgress);
|
||||
|
||||
appdb.update(app.id, values, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
for (var value in values) {
|
||||
app[value] = values[value];
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// Ordering is based on the following rationale:
|
||||
// - configure nginx, icon, oauth
|
||||
// - register subdomain.
|
||||
// at this point, the user can visit the site and the above nginx config can show some install screen.
|
||||
// the icon can be displayed in this nginx page and oauth proxy means the page can be protected
|
||||
// - download image
|
||||
// - setup volumes
|
||||
// - setup addons (requires the above volume)
|
||||
// - setup the container (requires image, volumes, addons)
|
||||
// - setup collectd (requires container id)
|
||||
function install(app, callback) {
|
||||
async.series([
|
||||
verifyManifest.bind(null, app),
|
||||
|
||||
// teardown for re-installs
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainer.bind(null, app),
|
||||
addons.teardownAddons.bind(null, app, app.manifest.addons),
|
||||
deleteVolume.bind(null, app),
|
||||
unregisterSubdomain.bind(null, app),
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
removeIcon.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '15, Configure nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '25, Creating OAuth proxy credentials' }),
|
||||
allocateOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
|
||||
downloadImage.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '50, Creating volume' }),
|
||||
createVolume.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '60, Setting up addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '80, Setting up collectd profile' }),
|
||||
addCollectdProfile.bind(null, app),
|
||||
|
||||
runApp.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'installed');
|
||||
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null }, callback);
|
||||
}
|
||||
], function seriesDone(error) {
|
||||
if (error) {
|
||||
debugApp(app, 'error installing app: %s', error);
|
||||
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
|
||||
}
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function backup(app, callback) {
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
|
||||
apps.backupApp.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'installed');
|
||||
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '' }, callback);
|
||||
}
|
||||
], function seriesDone(error) {
|
||||
if (error) {
|
||||
debugApp(app, 'error backing up app: %s', error);
|
||||
return updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: error.message }, callback.bind(null, error)); // return to installed state intentionally
|
||||
}
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// restore is also called for upgrades and infra updates. note that in those cases it is possible there is no backup
|
||||
function restore(app, callback) {
|
||||
// we don't have a backup, same as re-install
|
||||
if (!app.lastBackupId) return install(app, callback);
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainer.bind(null, app),
|
||||
// oldConfig can be null during upgrades
|
||||
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : null),
|
||||
deleteVolume.bind(null, app),
|
||||
deleteImage.bind(null, app, app.manifest),
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
removeIcon.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '30, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '50, Create OAuth proxy credentials' }),
|
||||
allocateOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '55, Registering subdomain' }), // ip might change during upgrades
|
||||
registerSubdomain.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '60, Downloading image' }),
|
||||
downloadImage.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '65, Creating volume' }),
|
||||
createVolume.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Download backup and restore addons' }),
|
||||
apps.restoreApp.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '75, Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '80, Setting up collectd profile' }),
|
||||
addCollectdProfile.bind(null, app),
|
||||
|
||||
runApp.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'restored');
|
||||
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null }, callback);
|
||||
}
|
||||
], function seriesDone(error) {
|
||||
if (error) {
|
||||
debugApp(app, 'Error installing app: %s', error);
|
||||
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// note that configure is called after an infra update as well
|
||||
function configure(app, callback) {
|
||||
var locationChanged = app.oldConfig.location !== app.location;
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainer.bind(null, app),
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
unregisterSubdomain(app, next);
|
||||
},
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '25, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '30, Create OAuth proxy credentials' }),
|
||||
allocateOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app)
|
||||
], next);
|
||||
},
|
||||
|
||||
// re-setup addons since they rely on the app's fqdn (e.g oauth)
|
||||
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '60, Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Add collectd profile' }),
|
||||
addCollectdProfile.bind(null, app),
|
||||
|
||||
runApp.bind(null, app),
|
||||
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app)
|
||||
], next);
|
||||
},
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'configured');
|
||||
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null }, callback);
|
||||
}
|
||||
], function seriesDone(error) {
|
||||
if (error) {
|
||||
debugApp(app, 'error reconfiguring : %s', error);
|
||||
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
|
||||
}
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// nginx configuration is skipped because app.httpPort is expected to be available
|
||||
function update(app, callback) {
|
||||
debugApp(app, 'Updating to %s', safe.query(app, 'manifest.version'));
|
||||
|
||||
// app does not want these addons anymore
|
||||
var unusedAddons = _.difference(Object.keys(app.oldConfig.manifest.addons), Object.keys(app.manifest.addons));
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '0, Verify manifest' }),
|
||||
verifyManifest.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '10, Backup app' }),
|
||||
function (done) {
|
||||
apps.backupApp(app, function (error) {
|
||||
if (error) error.backupError = true;
|
||||
done(error);
|
||||
});
|
||||
},
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainer.bind(null, app),
|
||||
addons.teardownAddons.bind(null, app, unusedAddons),
|
||||
deleteImage.bind(null, app, app.manifest), // delete image even if did not change (see df158b111f)
|
||||
removeIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '35, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '45, Downloading image' }),
|
||||
downloadImage.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Updating addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '80, Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Add collectd profile' }),
|
||||
addCollectdProfile.bind(null, app),
|
||||
|
||||
runApp.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'updated');
|
||||
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null }, callback);
|
||||
}
|
||||
], function seriesDone(error) {
|
||||
if (error && error.backupError) {
|
||||
// on a backup error, just abort the update
|
||||
debugApp(app, 'Error backing up app: %s', error.backupError);
|
||||
return updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null }, callback.bind(null, error));
|
||||
} else if (error) {
|
||||
debugApp(app, 'Error updating app: %s', error);
|
||||
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
|
||||
}
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function uninstall(app, callback) {
|
||||
debugApp(app, 'uninstalling');
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '0, Remove collectd profile' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '10, Stopping app' }),
|
||||
stopApp.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '20, Deleting container' }),
|
||||
deleteContainer.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '30, Teardown addons' }),
|
||||
addons.teardownAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Deleting volume' }),
|
||||
deleteVolume.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
|
||||
deleteImage.bind(null, app, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
|
||||
unregisterSubdomain.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Remove OAuth credentials' }),
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
|
||||
removeIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring Nginx' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
|
||||
appdb.del.bind(null, app.id)
|
||||
], callback);
|
||||
}
|
||||
|
||||
function runApp(app, callback) {
|
||||
startContainer(app, function (error) {
|
||||
if (error) {
|
||||
debugApp(app, 'Error starting container : %s', error);
|
||||
return updateApp(app, { runState: appdb.RSTATE_ERROR }, callback);
|
||||
}
|
||||
|
||||
updateApp(app, { runState: appdb.RSTATE_RUNNING }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function stopApp(app, callback) {
|
||||
stopContainer(app, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
updateApp(app, { runState: appdb.RSTATE_STOPPED }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function handleRunCommand(app, callback) {
|
||||
if (app.runState === appdb.RSTATE_PENDING_STOP) {
|
||||
return stopApp(app, callback);
|
||||
}
|
||||
|
||||
if (app.runState === appdb.RSTATE_PENDING_START || app.runState === appdb.RSTATE_RUNNING) {
|
||||
debugApp(app, 'Resuming app with state : %s', app.runState);
|
||||
return runApp(app, callback);
|
||||
}
|
||||
|
||||
debugApp(app, 'handleRunCommand - doing nothing: %s', app.runState);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function startTask(appId, callback) {
|
||||
// determine what to do
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'startTask installationState: %s runState: %s', app.installationState, app.runState);
|
||||
|
||||
if (app.installationState === appdb.ISTATE_PENDING_UNINSTALL) {
|
||||
return uninstall(app, callback);
|
||||
}
|
||||
|
||||
if (app.installationState === appdb.ISTATE_PENDING_CONFIGURE) {
|
||||
return configure(app, callback);
|
||||
}
|
||||
|
||||
if (app.installationState === appdb.ISTATE_PENDING_UPDATE) {
|
||||
return update(app, callback);
|
||||
}
|
||||
|
||||
if (app.installationState === appdb.ISTATE_PENDING_RESTORE) {
|
||||
return restore(app, callback);
|
||||
}
|
||||
|
||||
if (app.installationState === appdb.ISTATE_PENDING_BACKUP) {
|
||||
return backup(app, callback);
|
||||
}
|
||||
|
||||
if (app.installationState === appdb.ISTATE_INSTALLED) {
|
||||
return handleRunCommand(app, callback);
|
||||
}
|
||||
|
||||
if (app.installationState === appdb.ISTATE_PENDING_INSTALL) {
|
||||
return install(app, callback);
|
||||
}
|
||||
|
||||
debugApp(app, 'Apptask launched but nothing to do.');
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
assert.strictEqual(process.argv.length, 3, 'Pass the appid as argument');
|
||||
|
||||
debug('Apptask for %s', process.argv[2]);
|
||||
|
||||
initialize(function (error) {
|
||||
if (error) throw error;
|
||||
|
||||
startTask(process.argv[2], function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
debug('Apptask completed for %s', process.argv[2]);
|
||||
// https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below
|
||||
// to check apptask crashes
|
||||
process.exit(error ? 50 : 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
139
src/auth.js
Normal file
139
src/auth.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
BasicStrategy = require('passport-http').BasicStrategy,
|
||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||
clientdb = require('./clientdb'),
|
||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
DatabaseError = require('./databaseerror'),
|
||||
debug = require('debug')('box:auth'),
|
||||
LocalStrategy = require('passport-local').Strategy,
|
||||
crypto = require('crypto'),
|
||||
passport = require('passport'),
|
||||
tokendb = require('./tokendb'),
|
||||
user = require('./user'),
|
||||
userdb = require('./userdb'),
|
||||
UserError = user.UserError,
|
||||
_ = require('underscore');
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
passport.serializeUser(function (user, callback) {
|
||||
callback(null, user.username);
|
||||
});
|
||||
|
||||
passport.deserializeUser(function(username, callback) {
|
||||
userdb.get(username, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var md5 = crypto.createHash('md5').update(result.email.toLowerCase()).digest('hex');
|
||||
result.gravatar = 'https://www.gravatar.com/avatar/' + md5 + '.jpg?s=24&d=mm';
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
|
||||
passport.use(new LocalStrategy(function (username, password, callback) {
|
||||
if (username.indexOf('@') === -1) {
|
||||
user.verify(username, password, function (error, result) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (!result) return callback(null, false);
|
||||
callback(null, _.pick(result, 'id', 'username', 'email', 'admin'));
|
||||
});
|
||||
} else {
|
||||
user.verifyWithEmail(username, password, function (error, result) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (!result) return callback(null, false);
|
||||
callback(null, _.pick(result, 'id', 'username', 'email', 'admin'));
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
passport.use(new BasicStrategy(function (username, password, callback) {
|
||||
if (username.indexOf('cid-') === 0) {
|
||||
debug('BasicStrategy: detected client id %s instead of username:password', username);
|
||||
// username is actually client id here
|
||||
// password is client secret
|
||||
clientdb.get(username, function (error, client) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (client.clientSecret != password) return callback(null, false);
|
||||
return callback(null, client);
|
||||
});
|
||||
} else {
|
||||
user.verify(username, password, function (error, result) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (!result) return callback(null, false);
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
|
||||
clientdb.get(clientId, function(error, client) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) { return callback(error); }
|
||||
if (client.clientSecret != clientSecret) { return callback(null, false); }
|
||||
return callback(null, client);
|
||||
});
|
||||
}));
|
||||
|
||||
passport.use(new BearerStrategy(function (accessToken, callback) {
|
||||
tokendb.get(accessToken, function (error, token) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
// scopes here can define what capabilities that token carries
|
||||
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
|
||||
var info = { scope: token.scope };
|
||||
var tokenType;
|
||||
|
||||
if (token.identifier.indexOf(tokendb.PREFIX_DEV) === 0) {
|
||||
token.identifier = token.identifier.slice(tokendb.PREFIX_DEV.length);
|
||||
tokenType = tokendb.TYPE_DEV;
|
||||
} else if (token.identifier.indexOf(tokendb.PREFIX_APP) === 0) {
|
||||
tokenType = tokendb.TYPE_APP;
|
||||
return callback(null, { id: token.identifier.slice(tokendb.PREFIX_APP.length), tokenType: tokenType }, info);
|
||||
} else if (token.identifier.indexOf(tokendb.PREFIX_USER) === 0) {
|
||||
tokenType = tokendb.TYPE_USER;
|
||||
token.identifier = token.identifier.slice(tokendb.PREFIX_USER.length);
|
||||
} else {
|
||||
// legacy tokens assuming a user access token
|
||||
tokenType = tokendb.TYPE_USER;
|
||||
}
|
||||
|
||||
userdb.get(token.identifier, function (error, user) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
// amend the tokenType of the token owner
|
||||
user.tokenType = tokenType;
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
78
src/authcodedb.js
Normal file
78
src/authcodedb.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
add: add,
|
||||
del: del,
|
||||
delExpired: delExpired,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
|
||||
var AUTHCODES_FIELDS = [ 'authCode', 'userId', 'clientId', 'expiresAt' ].join(',');
|
||||
|
||||
function get(authCode, callback) {
|
||||
assert.strictEqual(typeof authCode, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + AUTHCODES_FIELDS + ' FROM authcodes WHERE authCode = ? AND expiresAt > ?', [ authCode, Date.now() ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function add(authCode, clientId, userId, expiresAt, callback) {
|
||||
assert.strictEqual(typeof authCode, 'string');
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof expiresAt, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO authcodes (authCode, clientId, userId, expiresAt) VALUES (?, ?, ?, ?)',
|
||||
[ authCode, clientId, userId, expiresAt ], function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(authCode, callback) {
|
||||
assert.strictEqual(typeof authCode, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM authcodes WHERE authCode = ?', [ authCode ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function delExpired(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM authcodes WHERE expiresAt <= ?', [ Date.now() ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
return callback(null, result.affectedRows);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM authcodes', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
95
src/backups.js
Normal file
95
src/backups.js
Normal file
@@ -0,0 +1,95 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
BackupsError: BackupsError,
|
||||
|
||||
getAllPaged: getAllPaged,
|
||||
|
||||
getBackupUrl: getBackupUrl,
|
||||
getRestoreUrl: getRestoreUrl
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:backups'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
function BackupsError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(BackupsError, Error);
|
||||
BackupsError.EXTERNAL_ERROR = 'external error';
|
||||
BackupsError.INTERNAL_ERROR = 'internal error';
|
||||
|
||||
function getAllPaged(page, perPage, callback) {
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backups';
|
||||
|
||||
superagent.get(url).query({ token: config.token() }).end(function (error, result) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 200) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
|
||||
if (!result.body || !util.isArray(result.body.backups)) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response'));
|
||||
|
||||
// [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first)
|
||||
return callback(null, result.body.backups);
|
||||
});
|
||||
}
|
||||
|
||||
function getBackupUrl(app, appBackupIds, callback) {
|
||||
assert(!app || typeof app === 'object');
|
||||
assert(!appBackupIds || util.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backupurl';
|
||||
|
||||
var data = {
|
||||
boxVersion: config.version(),
|
||||
appId: app ? app.id : null,
|
||||
appVersion: app ? app.manifest.version : null,
|
||||
appBackupIds: appBackupIds
|
||||
};
|
||||
|
||||
superagent.put(url).query({ token: config.token() }).send(data).end(function (error, result) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 201) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
|
||||
if (!result.body || !result.body.url) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response'));
|
||||
|
||||
return callback(null, result.body);
|
||||
});
|
||||
}
|
||||
|
||||
function getRestoreUrl(backupId, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/restoreurl';
|
||||
|
||||
superagent.put(url).query({ token: config.token(), backupId: backupId }).end(function (error, result) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 201) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
|
||||
if (!result.body || !result.body.url) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response'));
|
||||
|
||||
return callback(null, result.body);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
139
src/clientdb.js
Normal file
139
src/clientdb.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getAll: getAll,
|
||||
getAllWithTokenCountByIdentifier: getAllWithTokenCountByIdentifier,
|
||||
add: add,
|
||||
del: del,
|
||||
update: update,
|
||||
getByAppId: getByAppId,
|
||||
delByAppId: delByAppId,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror.js');
|
||||
|
||||
var CLIENTS_FIELDS = [ 'id', 'appId', 'clientSecret', 'redirectURI', 'scope' ].join(',');
|
||||
var CLIENTS_FIELDS_PREFIXED = [ 'clients.id', 'clients.appId', 'clients.clientSecret', 'clients.redirectURI', 'clients.scope' ].join(',');
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE id = ?', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients ORDER BY appId', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function getAllWithTokenCountByIdentifier(identifier, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId WHERE tokens.identifier=? GROUP BY clients.id', [ identifier ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
callback(null, results);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function getByAppId(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE appId = ? LIMIT 1', [ appId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
return callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appId, clientSecret, redirectURI, scope, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof clientSecret, 'string');
|
||||
assert.strictEqual(typeof redirectURI, 'string');
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [ id, appId, clientSecret, redirectURI, scope ];
|
||||
|
||||
database.query('INSERT INTO clients (id, appId, clientSecret, redirectURI, scope) VALUES (?, ?, ?, ?, ?)', data, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error || result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function update(id, appId, clientSecret, redirectURI, scope, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof clientSecret, 'string');
|
||||
assert.strictEqual(typeof redirectURI, 'string');
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [ appId, clientSecret, redirectURI, scope, id ];
|
||||
|
||||
database.query('UPDATE clients SET appId = ?, clientSecret = ?, redirectURI = ?, scope = ? WHERE id = ?', data, function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM clients WHERE id = ?', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function delByAppId(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM clients WHERE appId=?', [ appId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM clients WHERE appId!="webadmin"', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
226
src/clients.js
Normal file
226
src/clients.js
Normal file
@@ -0,0 +1,226 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
ClientsError: ClientsError,
|
||||
|
||||
add: add,
|
||||
get: get,
|
||||
update: update,
|
||||
del: del,
|
||||
getAllWithDetailsByUserId: getAllWithDetailsByUserId,
|
||||
getClientTokensByUserId: getClientTokensByUserId,
|
||||
delClientTokensByUserId: delClientTokensByUserId
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
util = require('util'),
|
||||
hat = require('hat'),
|
||||
appdb = require('./appdb.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
constants = require('./constants.js'),
|
||||
async = require('async'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
uuid = require('node-uuid');
|
||||
|
||||
function ClientsError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(ClientsError, Error);
|
||||
ClientsError.INVALID_SCOPE = 'Invalid scope';
|
||||
|
||||
function validateScope(scope) {
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
|
||||
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE);
|
||||
if (scope === '*') return null;
|
||||
|
||||
// TODO maybe validate all individual scopes if they exist
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function add(appIdentifier, redirectURI, scope, callback) {
|
||||
assert.strictEqual(typeof appIdentifier, 'string');
|
||||
assert.strictEqual(typeof redirectURI, 'string');
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateScope(scope);
|
||||
if (error) return callback(error);
|
||||
|
||||
var id = 'cid-' + uuid.v4();
|
||||
var clientSecret = hat(256);
|
||||
|
||||
clientdb.add(id, appIdentifier, clientSecret, redirectURI, scope, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var client = {
|
||||
id: id,
|
||||
appId: appIdentifier,
|
||||
clientSecret: clientSecret,
|
||||
redirectURI: redirectURI,
|
||||
scope: scope
|
||||
};
|
||||
|
||||
callback(null, client);
|
||||
});
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.get(id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
// we only allow appIdentifier and redirectURI to be updated
|
||||
function update(id, appIdentifier, redirectURI, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appIdentifier, 'string');
|
||||
assert.strictEqual(typeof redirectURI, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.get(id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
clientdb.update(id, appIdentifier, result.clientSecret, redirectURI, result.scope, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.del(id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getAllWithDetailsByUserId(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.getAllWithTokenCountByIdentifier(tokendb.PREFIX_USER + userId, function (error, results) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, []);
|
||||
if (error) return callback(error);
|
||||
|
||||
// We have several types of records here
|
||||
// 1) webadmin has an app id of 'webadmin'
|
||||
// 2) oauth proxy records are always the app id prefixed with 'proxy-'
|
||||
// 3) addon oauth records for apps prefixed with 'addon-'
|
||||
// 4) external app records prefixed with 'external-'
|
||||
// 5) normal apps on the cloudron without a prefix
|
||||
|
||||
var tmp = [];
|
||||
async.each(results, function (record, callback) {
|
||||
if (record.appId === constants.ADMIN_CLIENT_ID) {
|
||||
record.name = constants.ADMIN_NAME;
|
||||
record.location = constants.ADMIN_LOCATION;
|
||||
record.type = 'webadmin';
|
||||
|
||||
tmp.push(record);
|
||||
|
||||
return callback(null);
|
||||
} else if (record.appId === constants.TEST_CLIENT_ID) {
|
||||
record.name = constants.TEST_NAME;
|
||||
record.location = constants.TEST_LOCATION;
|
||||
record.type = 'test';
|
||||
|
||||
tmp.push(record);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
var appId = record.appId;
|
||||
var type = 'app';
|
||||
|
||||
// Handle our different types of oauth clients
|
||||
if (record.appId.indexOf('addon-') === 0) {
|
||||
appId = record.appId.slice('addon-'.length);
|
||||
type = 'addon';
|
||||
} else if (record.appId.indexOf('proxy-') === 0) {
|
||||
appId = record.appId.slice('proxy-'.length);
|
||||
type = 'proxy';
|
||||
}
|
||||
|
||||
appdb.get(appId, function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to get app details for oauth client', result, error);
|
||||
return callback(null); // ignore error so we continue listing clients
|
||||
}
|
||||
|
||||
record.name = result.manifest.title + (record.appId.indexOf('proxy-') === 0 ? 'OAuth Proxy' : '');
|
||||
record.location = result.location;
|
||||
record.type = type;
|
||||
|
||||
tmp.push(record);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
callback(null, tmp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getClientTokensByUserId(clientId, userId, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.getByIdentifierAndClientId(tokendb.PREFIX_USER + userId, clientId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) {
|
||||
// this can mean either that there are no tokens or the clientId is actually unknown
|
||||
clientdb.get(clientId, function (error/*, result*/) {
|
||||
if (error) return callback(error);
|
||||
callback(null, []);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (error) return callback(error);
|
||||
callback(null, result || []);
|
||||
});
|
||||
}
|
||||
|
||||
function delClientTokensByUserId(clientId, userId, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.delByIdentifierAndClientId(tokendb.PREFIX_USER + userId, clientId, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) {
|
||||
// this can mean either that there are no tokens or the clientId is actually unknown
|
||||
clientdb.get(clientId, function (error/*, result*/) {
|
||||
if (error) return callback(error);
|
||||
callback(null);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (error) return callback(error);
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
615
src/cloudron.js
Normal file
615
src/cloudron.js
Normal file
@@ -0,0 +1,615 @@
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
CloudronError: CloudronError,
|
||||
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
activate: activate,
|
||||
getConfig: getConfig,
|
||||
getStatus: getStatus,
|
||||
|
||||
setCertificate: setCertificate,
|
||||
|
||||
sendHeartbeat: sendHeartbeat,
|
||||
|
||||
update: update,
|
||||
reboot: reboot,
|
||||
migrate: migrate,
|
||||
backup: backup,
|
||||
ensureBackup: ensureBackup
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
AppsError = require('./apps.js').AppsError,
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
clientdb = require('./clientdb.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
progress = require('./progress.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
SettingsError = settings.SettingsError,
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
user = require('./user.js'),
|
||||
UserError = user.UserError,
|
||||
userdb = require('./userdb.js'),
|
||||
util = require('util');
|
||||
|
||||
var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
|
||||
REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
|
||||
BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
|
||||
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
|
||||
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update';
|
||||
|
||||
var gAddMailDnsRecordsTimerId = null,
|
||||
gCloudronDetails = null; // cached cloudron details like region,size...
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
var prefix = app ? app.location : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function ignoreError(func) {
|
||||
return function (callback) {
|
||||
func(function (error) {
|
||||
if (error) console.error('Ignored error:', error);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function CloudronError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(CloudronError, Error);
|
||||
CloudronError.BAD_FIELD = 'Field error';
|
||||
CloudronError.INTERNAL_ERROR = 'Internal Error';
|
||||
CloudronError.EXTERNAL_ERROR = 'External Error';
|
||||
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
|
||||
CloudronError.BAD_USERNAME = 'Bad username';
|
||||
CloudronError.BAD_EMAIL = 'Bad email';
|
||||
CloudronError.BAD_PASSWORD = 'Bad password';
|
||||
CloudronError.BAD_NAME = 'Bad name';
|
||||
CloudronError.BAD_STATE = 'Bad state';
|
||||
CloudronError.NOT_FOUND = 'Not found';
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
addMailDnsRecords();
|
||||
}
|
||||
|
||||
// Send heartbeat once we are up and running, this speeds up the Cloudron creation, as otherwise we are bound to the cron.js settings
|
||||
sendHeartbeat();
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clearTimeout(gAddMailDnsRecordsTimerId);
|
||||
gAddMailDnsRecordsTimerId = null;
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function setTimeZone(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('setTimeZone ip:%s', ip);
|
||||
|
||||
superagent.get('http://www.telize.com/geoip/' + ip).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
debug('Failed to get geo location', error);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
if (!result.body.timezone) {
|
||||
debug('No timezone in geoip response');
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
debug('Setting timezone to ', result.body.timezone);
|
||||
|
||||
settings.setTimeZone(result.body.timezone, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function activate(username, password, email, name, ip, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert(!name || typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('activating user:%s email:%s', username, email);
|
||||
|
||||
setTimeZone(ip, function () { });
|
||||
|
||||
if (!name) name = settings.getDefaultSync(settings.CLOUDRON_NAME_KEY);
|
||||
|
||||
settings.setCloudronName(name, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_NAME));
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
user.createOwner(username, password, email, function (error, userObject) {
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
|
||||
if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME));
|
||||
if (error && error.reason === UserError.BAD_PASSWORD) return callback(new CloudronError(CloudronError.BAD_PASSWORD));
|
||||
if (error && error.reason === UserError.BAD_EMAIL) return callback(new CloudronError(CloudronError.BAD_EMAIL));
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
clientdb.getByAppId('webadmin', function (error, result) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
// Also generate a token so the admin creation can also act as a login
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
|
||||
|
||||
tokendb.add(token, tokendb.PREFIX_USER + userObject.id, result.id, expires, '*', function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, { token: token, expires: expires });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.count(function (error, count) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
activated: count !== 0,
|
||||
version: config.version(),
|
||||
cloudronName: cloudronName
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getCloudronDetails(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gCloudronDetails) return callback(null, gCloudronDetails);
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
|
||||
.query({ token: config.token() })
|
||||
.end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.status !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
gCloudronDetails = result.body.box;
|
||||
|
||||
return callback(null, gCloudronDetails);
|
||||
});
|
||||
}
|
||||
|
||||
function getConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronDetails(function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to fetch cloudron details.', error);
|
||||
|
||||
// set fallback values to avoid dependency on appstore
|
||||
result = {
|
||||
region: result ? result.region : null,
|
||||
size: result ? result.size : null
|
||||
};
|
||||
}
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
isDev: config.isDev(),
|
||||
fqdn: config.fqdn(),
|
||||
ip: sysinfo.getIp(),
|
||||
version: config.version(),
|
||||
update: updateChecker.getUpdateInfo(),
|
||||
progress: progress.get(),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
developerMode: config.developerMode(),
|
||||
region: result.region,
|
||||
size: result.size,
|
||||
cloudronName: cloudronName
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
||||
debug('Sending heartbeat ' + url);
|
||||
|
||||
// TODO: this must be a POST
|
||||
superagent.get(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
|
||||
if (error) debug('Error sending heartbeat.', error);
|
||||
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
|
||||
else debug('Heartbeat successful');
|
||||
});
|
||||
}
|
||||
|
||||
function sendMailDnsRecordsRequest(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var DKIM_SELECTOR = 'mail';
|
||||
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
|
||||
|
||||
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
|
||||
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
|
||||
|
||||
if (publicKey === null) return callback(new Error('Error reading dkim public key'));
|
||||
|
||||
// remove header, footer and new lines
|
||||
publicKey = publicKey.split('\n').slice(1, -2).join('');
|
||||
|
||||
// note that dmarc requires special DNS records for external RUF and RUA
|
||||
var records = [
|
||||
// softfail all mails not from our IP. Note that this uses IP instead of 'a' should we use a load balancer in the future
|
||||
{ subdomain: '', type: 'TXT', value: '"v=spf1 ip4:' + sysinfo.getIp() + ' ~all"' },
|
||||
// t=s limits the domainkey to this domain and not it's subdomains
|
||||
{ subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', value: '"v=DKIM1; t=s; p=' + publicKey + '"' },
|
||||
// DMARC requires special setup if report email id is in different domain
|
||||
{ subdomain: '_dmarc', type: 'TXT', value: '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' }
|
||||
];
|
||||
|
||||
debug('sendMailDnsRecords request:%s', JSON.stringify(records));
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/subdomains')
|
||||
.set('Accept', 'application/json')
|
||||
.query({ token: config.token() })
|
||||
.send({ records: records })
|
||||
.end(function (error, res) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('sendMailDnsRecords status: %s', res.status);
|
||||
|
||||
if (res.status === 409) return callback(null); // already registered
|
||||
|
||||
if (res.status !== 201) return callback(new Error(util.format('Failed to add Mail DNS records: %s %j', res.status, res.body)));
|
||||
|
||||
return callback(null, res.body.ids);
|
||||
});
|
||||
}
|
||||
|
||||
function addMailDnsRecords() {
|
||||
if (config.get('mailDnsRecordIds').length !== 0) return; // already registered
|
||||
|
||||
sendMailDnsRecordsRequest(function (error, ids) {
|
||||
if (error) {
|
||||
console.error('Mail DNS record addition failed', error);
|
||||
gAddMailDnsRecordsTimerId = setTimeout(addMailDnsRecords, 30000);
|
||||
return;
|
||||
}
|
||||
|
||||
debug('Added Mail DNS records successfully');
|
||||
config.set('mailDnsRecordIds', ids);
|
||||
});
|
||||
}
|
||||
|
||||
function setCertificate(certificate, key, callback) {
|
||||
assert.strictEqual(typeof certificate, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Updating certificates');
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), certificate)) {
|
||||
return callback(new CloudronError(CloudronError.INTERNAL_ERROR, safe.error.message));
|
||||
}
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) {
|
||||
return callback(new CloudronError(CloudronError.INTERNAL_ERROR, safe.error.message));
|
||||
}
|
||||
|
||||
shell.sudo('setCertificate', [ RELOAD_NGINX_CMD ], function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function reboot(callback) {
|
||||
shell.sudo('reboot', [ REBOOT_CMD ], callback);
|
||||
}
|
||||
|
||||
function migrate(size, region, callback) {
|
||||
assert.strictEqual(typeof size, 'string');
|
||||
assert.strictEqual(typeof region, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = locker.lock(locker.OP_MIGRATE);
|
||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
||||
|
||||
function unlock(error) {
|
||||
if (error) {
|
||||
debug('Failed to migrate', error);
|
||||
locker.unlock(locker.OP_MIGRATE);
|
||||
} else {
|
||||
debug('Migration initiated successfully');
|
||||
// do not unlock; cloudron is migrating
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// initiate the migration in the background
|
||||
backupBoxAndApps(function (error, restoreKey) {
|
||||
if (error) return unlock(error);
|
||||
|
||||
debug('migrate: size %s region %s restoreKey %s', size, region, restoreKey);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
|
||||
.query({ token: config.token() })
|
||||
.send({ size: size, region: region, restoreKey: restoreKey })
|
||||
.end(function (error, result) {
|
||||
if (error) return unlock(error);
|
||||
if (result.status === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
|
||||
if (result.status === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
|
||||
if (result.status !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
return unlock(null);
|
||||
});
|
||||
});
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function update(boxUpdateInfo, callback) {
|
||||
assert.strictEqual(typeof boxUpdateInfo, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!boxUpdateInfo) return callback(null);
|
||||
|
||||
var error = locker.lock(locker.OP_BOX_UPDATE);
|
||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
||||
|
||||
progress.set(progress.UPDATE, 0, 'Begin ' + (boxUpdateInfo.update ? 'upgrade': 'update'));
|
||||
|
||||
// initiate the update/upgrade but do not wait for it
|
||||
if (boxUpdateInfo.upgrade) {
|
||||
doUpgrade(boxUpdateInfo, function (error) {
|
||||
locker.unlock(locker.OP_BOX_UPDATE);
|
||||
});
|
||||
} else {
|
||||
doUpdate(boxUpdateInfo, function (error) {
|
||||
locker.unlock(locker.OP_BOX_UPDATE);
|
||||
});
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function doUpgrade(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Create app and box backup');
|
||||
|
||||
backupBoxAndApps(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
|
||||
.query({ token: config.token() })
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.end(function (error, result) {
|
||||
if (error) return callback(new Error('Error making upgrade request: ' + error));
|
||||
if (result.status !== 202) return callback(new Error('Server not ready to upgrade: ' + result.body));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
|
||||
// no need to unlock since this is the last thing we ever do on this box
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doUpdate(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Create box backup');
|
||||
|
||||
backupBox(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// fetch a signed sourceTarballUrl
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/sourcetarballurl')
|
||||
.query({ token: config.token(), boxVersion: boxUpdateInfo.version })
|
||||
.end(function (error, result) {
|
||||
if (error) return callback(new Error('Error fetching sourceTarballUrl: ' + error));
|
||||
if (result.status !== 200) return callback(new Error('Error fetching sourceTarballUrl status: ' + result.status));
|
||||
if (!safe.query(result, 'body.url')) return callback(new Error('Error fetching sourceTarballUrl response: ' + result.body));
|
||||
|
||||
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
|
||||
var args = {
|
||||
sourceTarballUrl: result.body.url,
|
||||
|
||||
// this data is opaque to the installer
|
||||
data: {
|
||||
boxVersionsUrl: config.get('boxVersionsUrl'),
|
||||
version: boxUpdateInfo.version,
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
fqdn: config.fqdn(),
|
||||
token: config.token(),
|
||||
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
|
||||
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
restoreUrl: null,
|
||||
restoreKey: null,
|
||||
developerMode: config.developerMode() // this survives updates but not upgrades
|
||||
}
|
||||
};
|
||||
|
||||
debug('updating box %j', args);
|
||||
|
||||
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.status !== 202) return callback(new Error('Error initiating update: ' + result.body));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
|
||||
// Do not add any code here. The installer script will stop the box code any instant
|
||||
});
|
||||
}
|
||||
|
||||
function backup(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = locker.lock(locker.OP_FULL_BACKUP);
|
||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
||||
|
||||
// start the backup operation in the background
|
||||
backupBoxAndApps(function (error) {
|
||||
if (error) console.error('backup failed.', error);
|
||||
|
||||
locker.unlock(locker.OP_FULL_BACKUP);
|
||||
});
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function ensureBackup(callback) {
|
||||
callback = callback || function () { };
|
||||
|
||||
backups.getAllPaged(1, 1, function (error, backups) {
|
||||
if (error) {
|
||||
debug('Unable to list backups', error);
|
||||
return callback(error); // no point trying to backup if appstore is down
|
||||
}
|
||||
|
||||
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
|
||||
debug('Previous backup was %j, no need to backup now', backups[0]);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
backup(callback);
|
||||
});
|
||||
}
|
||||
|
||||
function backupBoxWithAppBackupIds(appBackupIds, callback) {
|
||||
assert(util.isArray(appBackupIds));
|
||||
|
||||
backups.getBackupUrl(null /* app */, appBackupIds, function (error, result) {
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new CloudronError.INTERNAL_ERROR, error);
|
||||
|
||||
debug('backup: url %s', result.url);
|
||||
|
||||
async.series([
|
||||
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
|
||||
shell.sudo.bind(null, 'backupBox', [ BACKUP_BOX_CMD, result.url, result.backupKey ]),
|
||||
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
|
||||
], function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
debug('backup: successful');
|
||||
|
||||
callback(null, result.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// this function expects you to have a lock
|
||||
function backupBox(callback) {
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
var appBackupIds = allApps.map(function (app) { return app.lastBackupId; });
|
||||
appBackupIds = appBackupIds.filter(function (id) { return id !== null }); // remove apps that were never backed up
|
||||
|
||||
backupBoxWithAppBackupIds(appBackupIds, callback);
|
||||
});
|
||||
}
|
||||
|
||||
// this function expects you to have a lock
|
||||
function backupBoxAndApps(callback) {
|
||||
callback = callback || function () { }; // callback can be empty for timer triggered backup
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
var processed = 0;
|
||||
var step = 100/(allApps.length+1);
|
||||
|
||||
progress.set(progress.BACKUP, processed, '');
|
||||
|
||||
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
|
||||
++processed;
|
||||
|
||||
apps.backupApp(app, function (error, backupId) {
|
||||
progress.set(progress.BACKUP, step * processed, app.location);
|
||||
|
||||
if (error && error.reason === AppsError.BAD_STATE) {
|
||||
debugApp(app, 'Skipping backup (istate:%s health%s). Reusing %s', app.installationState, app.health, app.lastBackupId);
|
||||
backupId = app.lastBackupId;
|
||||
}
|
||||
|
||||
return iteratorCallback(null, backupId);
|
||||
});
|
||||
}, function appsBackedUp(error, backupIds) {
|
||||
if (error) return callback(error);
|
||||
|
||||
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
|
||||
|
||||
backupBoxWithAppBackupIds(backupIds, function (error, restoreKey) {
|
||||
progress.set(progress.BACKUP, 100, '');
|
||||
callback(error, restoreKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
32
src/collectd.config.ejs
Normal file
32
src/collectd.config.ejs
Normal file
@@ -0,0 +1,32 @@
|
||||
LoadPlugin "table"
|
||||
<Plugin table>
|
||||
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.stat">
|
||||
Instance "<%= appId %>-memory"
|
||||
Separator " \\n"
|
||||
<Result>
|
||||
Type gauge
|
||||
InstancesFrom 0
|
||||
ValuesFrom 1
|
||||
</Result>
|
||||
</Table>
|
||||
|
||||
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.max_usage_in_bytes">
|
||||
Instance "<%= appId %>-memory"
|
||||
Separator "\\n"
|
||||
<Result>
|
||||
Type gauge
|
||||
InstancePrefix "max_usage_in_bytes"
|
||||
ValuesFrom 0
|
||||
</Result>
|
||||
</Table>
|
||||
|
||||
<Table "/sys/fs/cgroup/cpuacct/docker/<%= containerId %>/cpuacct.stat">
|
||||
Instance "<%= appId %>-cpu"
|
||||
Separator " \\n"
|
||||
<Result>
|
||||
Type gauge
|
||||
InstancesFrom 0
|
||||
ValuesFrom 1
|
||||
</Result>
|
||||
</Table>
|
||||
</Plugin>
|
||||
181
src/config.js
Normal file
181
src/config.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
baseDir: baseDir,
|
||||
|
||||
// values set here will be lost after a upgrade/update. use the sqlite database
|
||||
// for persistent values that need to be backed up
|
||||
get: get,
|
||||
set: set,
|
||||
|
||||
// ifdefs to check environment
|
||||
CLOUDRON: process.env.NODE_ENV === 'cloudron',
|
||||
TEST: process.env.NODE_ENV === 'test',
|
||||
|
||||
// convenience getters
|
||||
apiServerOrigin: apiServerOrigin,
|
||||
webServerOrigin: webServerOrigin,
|
||||
fqdn: fqdn,
|
||||
token: token,
|
||||
version: version,
|
||||
isCustomDomain: isCustomDomain,
|
||||
database: database,
|
||||
developerMode: developerMode,
|
||||
|
||||
// these values are derived
|
||||
adminOrigin: adminOrigin,
|
||||
appFqdn: appFqdn,
|
||||
zoneName: zoneName,
|
||||
|
||||
isDev: isDev,
|
||||
|
||||
// for testing resets to defaults
|
||||
_reset: initConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
_ = require('underscore');
|
||||
|
||||
var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
|
||||
var data = { };
|
||||
|
||||
function baseDir() {
|
||||
if (exports.CLOUDRON) return homeDir;
|
||||
if (exports.TEST) return path.join(homeDir, '.cloudron_test');
|
||||
}
|
||||
|
||||
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
|
||||
|
||||
function saveSync() {
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
|
||||
}
|
||||
|
||||
function initConfig() {
|
||||
// setup defaults
|
||||
data.fqdn = 'localhost';
|
||||
|
||||
data.token = null;
|
||||
data.mailServer = null;
|
||||
data.adminEmail = null;
|
||||
data.mailDnsRecordIds = [ ];
|
||||
data.boxVersionsUrl = null;
|
||||
data.version = null;
|
||||
data.isCustomDomain = false;
|
||||
data.webServerOrigin = null;
|
||||
data.internalPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
|
||||
if (exports.CLOUDRON) {
|
||||
data.port = 3000;
|
||||
data.apiServerOrigin = null;
|
||||
data.database = null;
|
||||
data.developerMode = false;
|
||||
} else if (exports.TEST) {
|
||||
data.port = 5454;
|
||||
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
|
||||
data.database = {
|
||||
hostname: 'localhost',
|
||||
username: 'root',
|
||||
password: '',
|
||||
port: 3306,
|
||||
name: 'boxtest'
|
||||
};
|
||||
data.token = 'APPSTORE_TOKEN';
|
||||
data.developerMode = false;
|
||||
} else {
|
||||
assert(false, 'Unknown environment. This should not happen!');
|
||||
}
|
||||
|
||||
if (safe.fs.existsSync(cloudronConfigFileName)) {
|
||||
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
|
||||
_.extend(data, existingData); // overwrite defaults with saved config
|
||||
return;
|
||||
}
|
||||
|
||||
saveSync();
|
||||
}
|
||||
|
||||
initConfig();
|
||||
|
||||
// set(obj) or set(key, value)
|
||||
function set(key, value) {
|
||||
if (typeof key === 'object') {
|
||||
var obj = key;
|
||||
for (var k in obj) {
|
||||
assert(k in data, 'config.js is missing key "' + k + '"');
|
||||
data[k] = obj[k];
|
||||
}
|
||||
} else {
|
||||
data = safe.set(data, key, value);
|
||||
}
|
||||
saveSync();
|
||||
}
|
||||
|
||||
function get(key) {
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
|
||||
return safe.query(data, key);
|
||||
}
|
||||
|
||||
function apiServerOrigin() {
|
||||
return get('apiServerOrigin');
|
||||
}
|
||||
|
||||
function webServerOrigin() {
|
||||
return get('webServerOrigin');
|
||||
}
|
||||
|
||||
function fqdn() {
|
||||
return get('fqdn');
|
||||
}
|
||||
|
||||
// keep this in sync with start.sh admin.conf generation code
|
||||
function appFqdn(location) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
|
||||
if (location === '') return fqdn();
|
||||
return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn();
|
||||
}
|
||||
|
||||
function adminOrigin() {
|
||||
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
|
||||
}
|
||||
|
||||
function token() {
|
||||
return get('token');
|
||||
}
|
||||
|
||||
function version() {
|
||||
return get('version');
|
||||
}
|
||||
|
||||
function isCustomDomain() {
|
||||
return get('isCustomDomain');
|
||||
}
|
||||
|
||||
function zoneName() {
|
||||
if (isCustomDomain()) return fqdn(); // the appstore sets up the custom domain as a zone
|
||||
|
||||
// for shared domain name, strip out the hostname
|
||||
return fqdn().substr(fqdn().indexOf('.') + 1);
|
||||
}
|
||||
|
||||
function database() {
|
||||
return get('database');
|
||||
}
|
||||
|
||||
function developerMode() {
|
||||
return get('developerMode');
|
||||
}
|
||||
|
||||
function isDev() {
|
||||
return /dev/i.test(get('boxVersionsUrl'));
|
||||
}
|
||||
|
||||
16
src/constants.js
Normal file
16
src/constants.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
// default admin installation location. keep in sync with ADMIN_LOCATION in setup/start.sh and BOX_ADMIN_LOCATION in appstore constants.js
|
||||
exports = module.exports = {
|
||||
ADMIN_LOCATION: 'my',
|
||||
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
|
||||
ADMIN_NAME: 'Settings',
|
||||
|
||||
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
|
||||
ADMIN_APPID: 'admin', // admin appid (settingsdb)
|
||||
|
||||
TEST_NAME: 'Test',
|
||||
TEST_LOCATION: '',
|
||||
TEST_CLIENT_ID: 'test'
|
||||
};
|
||||
|
||||
138
src/cron.js
Normal file
138
src/cron.js
Normal file
@@ -0,0 +1,138 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
debug = require('debug')('box:cron'),
|
||||
settings = require('./settings.js'),
|
||||
updateChecker = require('./updatechecker.js');
|
||||
|
||||
var gAutoupdaterJob = null,
|
||||
gBoxUpdateCheckerJob = null,
|
||||
gAppUpdateCheckerJob = null,
|
||||
gHeartbeatJob = null,
|
||||
gBackupJob = null;
|
||||
|
||||
var gInitialized = false;
|
||||
|
||||
var NOOP_CALLBACK = function (error) { console.error(error); };
|
||||
|
||||
// cron format
|
||||
// Seconds: 0-59
|
||||
// Minutes: 0-59
|
||||
// Hours: 0-23
|
||||
// Day of Month: 1-31
|
||||
// Months: 0-11
|
||||
// Day of Week: 0-6
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gInitialized) return callback();
|
||||
|
||||
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
|
||||
gInitialized = true;
|
||||
|
||||
recreateJobs(callback);
|
||||
}
|
||||
|
||||
function recreateJobs(unusedTimeZone, callback) {
|
||||
if (typeof unusedTimeZone === 'function') callback = unusedTimeZone;
|
||||
|
||||
settings.getAll(function (error, allSettings) {
|
||||
if (gHeartbeatJob) gHeartbeatJob.stop();
|
||||
gHeartbeatJob = new CronJob({
|
||||
cronTime: '00 */1 * * * *', // every minute
|
||||
onTick: cloudron.sendHeartbeat,
|
||||
start: true,
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
});
|
||||
|
||||
if (gBackupJob) gBackupJob.stop();
|
||||
gBackupJob = new CronJob({
|
||||
cronTime: '00 00 */4 * * *', // every 4 hours
|
||||
onTick: cloudron.ensureBackup,
|
||||
start: true,
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
});
|
||||
|
||||
if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop();
|
||||
gBoxUpdateCheckerJob = new CronJob({
|
||||
cronTime: '00 */10 * * * *', // every 10 minutes
|
||||
onTick: updateChecker.checkBoxUpdates,
|
||||
start: true,
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
});
|
||||
|
||||
if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop();
|
||||
gAppUpdateCheckerJob = new CronJob({
|
||||
cronTime: '00 */10 * * * *', // every 10 minutes
|
||||
onTick: updateChecker.checkAppUpdates,
|
||||
start: true,
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
});
|
||||
|
||||
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
|
||||
function autoupdatePatternChanged(pattern) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
|
||||
debug('Auto update pattern changed to %s', pattern);
|
||||
|
||||
if (gAutoupdaterJob) gAutoupdaterJob.stop();
|
||||
|
||||
if (pattern === 'never') return;
|
||||
|
||||
gAutoupdaterJob = new CronJob({
|
||||
cronTime: pattern,
|
||||
onTick: function() {
|
||||
debug('Starting autoupdate');
|
||||
var updateInfo = updateChecker.getUpdateInfo();
|
||||
if (updateInfo.box) {
|
||||
cloudron.update(updateInfo.box, NOOP_CALLBACK);
|
||||
} else if (updateInfo.apps) {
|
||||
apps.autoupdateApps(updateInfo.apps, NOOP_CALLBACK);
|
||||
}
|
||||
},
|
||||
start: true,
|
||||
timeZone: gBoxUpdateCheckerJob.cronTime.timeZone // hack
|
||||
});
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!gInitialized) return callback();
|
||||
|
||||
if (gAutoupdaterJob) gAutoupdaterJob.stop();
|
||||
gAutoupdaterJob = null;
|
||||
|
||||
gBoxUpdateCheckerJob.stop();
|
||||
gBoxUpdateCheckerJob = null;
|
||||
|
||||
gAppUpdateCheckerJob.stop();
|
||||
gAppUpdateCheckerJob = null;
|
||||
|
||||
gHeartbeatJob.stop();
|
||||
gHeartbeatJob = null;
|
||||
|
||||
gBackupJob.stop();
|
||||
gBackupJob = null;
|
||||
|
||||
gInitialized = false;
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
202
src/database.js
Normal file
202
src/database.js
Normal file
@@ -0,0 +1,202 @@
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
query: query,
|
||||
transaction: transaction,
|
||||
|
||||
beginTransaction: beginTransaction,
|
||||
rollback: rollback,
|
||||
commit: commit,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
once = require('once'),
|
||||
config = require('./config.js'),
|
||||
mysql = require('mysql'),
|
||||
util = require('util');
|
||||
|
||||
var gConnectionPool = null,
|
||||
gDefaultConnection = null;
|
||||
|
||||
function initialize(options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {
|
||||
connectionLimit: 5
|
||||
};
|
||||
}
|
||||
|
||||
assert.strictEqual(typeof options.connectionLimit, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gConnectionPool !== null) return callback(null);
|
||||
|
||||
gConnectionPool = mysql.createPool({
|
||||
connectionLimit: options.connectionLimit,
|
||||
host: config.database().hostname,
|
||||
user: config.database().username,
|
||||
password: config.database().password,
|
||||
port: config.database().port,
|
||||
database: config.database().name,
|
||||
multipleStatements: false,
|
||||
ssl: false
|
||||
});
|
||||
|
||||
reconnect(callback);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
if (gConnectionPool) {
|
||||
gConnectionPool.end(callback);
|
||||
gConnectionPool = null;
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
|
||||
function setupConnection(connection, callback) {
|
||||
assert.strictEqual(typeof connection, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
connection.on('error', console.error);
|
||||
|
||||
async.series([
|
||||
connection.query.bind(connection, 'USE ' + config.database().name),
|
||||
connection.query.bind(connection, 'SET SESSION sql_mode = \'strict_all_tables\'')
|
||||
], function (error) {
|
||||
connection.removeListener('error', console.error);
|
||||
|
||||
if (error) connection.release();
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function reconnect(callback) {
|
||||
callback = callback ? once(callback) : function () {};
|
||||
|
||||
gConnectionPool.getConnection(function (error, connection) {
|
||||
if (error) {
|
||||
console.error('Unable to reestablish connection to database. Try again in a bit.', error.message);
|
||||
return setTimeout(reconnect.bind(null, callback), 1000);
|
||||
}
|
||||
|
||||
connection.on('error', function (error) {
|
||||
// by design, we catch all normal errors by providing callbacks.
|
||||
// this function should be invoked only when we have no callbacks pending and we have a fatal error
|
||||
assert(error.fatal, 'Non-fatal error on connection object');
|
||||
|
||||
console.error('Unhandled mysql connection error.', error);
|
||||
|
||||
// This is most likely an issue an can cause double callbacks from reconnect()
|
||||
setTimeout(reconnect.bind(null, callback), 1000);
|
||||
});
|
||||
|
||||
setupConnection(connection, function (error) {
|
||||
if (error) return setTimeout(reconnect.bind(null, callback), 1000);
|
||||
|
||||
gDefaultConnection = connection;
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// the clear funcs don't completely clear the db, they leave the migration code defaults
|
||||
async.series([
|
||||
require('./appdb.js')._clear,
|
||||
require('./authcodedb.js')._clear,
|
||||
require('./clientdb.js')._clear,
|
||||
require('./tokendb.js')._clear,
|
||||
require('./userdb.js')._clear,
|
||||
require('./settingsdb.js')._clear
|
||||
], callback);
|
||||
}
|
||||
|
||||
function beginTransaction(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gConnectionPool.getConnection(function (error, connection) {
|
||||
if (error) return callback(error);
|
||||
|
||||
setupConnection(connection, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
connection.beginTransaction(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
return callback(null, connection);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function rollback(connection, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
connection.rollback(function (error) {
|
||||
if (error) console.error(error); // can this happen?
|
||||
|
||||
connection.release();
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: if commit fails, is it supposed to return an error ?
|
||||
function commit(connection, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
connection.commit(function (error) {
|
||||
if (error) return rollback(connection, callback);
|
||||
|
||||
connection.release();
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function query() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var callback = args[args.length - 1];
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gDefaultConnection === null) return callback(new Error('No connection to database'));
|
||||
|
||||
args[args.length -1 ] = function (error, result) {
|
||||
if (error && error.fatal) {
|
||||
gDefaultConnection = null;
|
||||
setTimeout(reconnect, 1000);
|
||||
}
|
||||
|
||||
callback(error, result);
|
||||
};
|
||||
|
||||
gDefaultConnection.query.apply(gDefaultConnection, args);
|
||||
}
|
||||
|
||||
function transaction(queries, callback) {
|
||||
assert(util.isArray(queries));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
beginTransaction(function (error, conn) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.mapSeries(queries, function iterator(query, done) {
|
||||
conn.query(query.query, query.args, done);
|
||||
}, function seriesDone(error, results) {
|
||||
if (error) return rollback(conn, callback.bind(null, error));
|
||||
|
||||
commit(conn, callback.bind(null, null, results));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
32
src/databaseerror.js
Normal file
32
src/databaseerror.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = DatabaseError;
|
||||
|
||||
var assert = require('assert'),
|
||||
util = require('util');
|
||||
|
||||
function DatabaseError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined' || errorOrMessage === null);
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined' || errorOrMessage === null) {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(DatabaseError, Error);
|
||||
|
||||
DatabaseError.INTERNAL_ERROR = 'Internal error';
|
||||
DatabaseError.ALREADY_EXISTS = 'Entry already exist';
|
||||
DatabaseError.NOT_FOUND = 'Record not found';
|
||||
DatabaseError.BAD_FIELD = 'Invalid field';
|
||||
66
src/developer.js
Normal file
66
src/developer.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
DeveloperError: DeveloperError,
|
||||
|
||||
enabled: enabled,
|
||||
setEnabled: setEnabled,
|
||||
issueDeveloperToken: issueDeveloperToken
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
config = require('./config.js'),
|
||||
util = require('util');
|
||||
|
||||
function DeveloperError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(DeveloperError, Error);
|
||||
DeveloperError.INTERNAL_ERROR = 'Internal Error';
|
||||
|
||||
function enabled(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback(null, config.developerMode());
|
||||
}
|
||||
|
||||
function setEnabled(enabled, callback) {
|
||||
assert.strictEqual(typeof enabled, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
config.set('developerMode', enabled);
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function issueDeveloperToken(user, callback) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var expiresAt = Date.now() + 24 * 60 * 60 * 1000; // 1 day
|
||||
|
||||
tokendb.add(token, tokendb.PREFIX_DEV + user.id, '', expiresAt, 'apps,settings,roleDeveloper', function (error) {
|
||||
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, { token: token, expiresAt: expiresAt });
|
||||
});
|
||||
}
|
||||
46
src/digitalocean.js
Normal file
46
src/digitalocean.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
checkPtrRecord: checkPtrRecord
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:digitalocean'),
|
||||
dns = require('native-dns');
|
||||
|
||||
function checkPtrRecord(ip, fqdn, callback) {
|
||||
assert(ip === null || typeof ip === 'string');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('checkPtrRecord: ' + ip);
|
||||
|
||||
if (!ip) return callback(new Error('Network down'));
|
||||
|
||||
dns.resolve4('ns1.digitalocean.com', function (error, rdnsIps) {
|
||||
if (error || rdnsIps.length === 0) return callback(new Error('Failed to query DO DNS'));
|
||||
|
||||
var reversedIp = ip.split('.').reverse().join('.');
|
||||
|
||||
var req = dns.Request({
|
||||
question: dns.Question({ name: reversedIp + '.in-addr.arpa', type: 'PTR' }),
|
||||
server: { address: rdnsIps[0] },
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
req.on('timeout', function () { return callback(new Error('Timedout')); });
|
||||
|
||||
req.on('message', function (error, message) {
|
||||
if (error || !message.answer || message.answer.length === 0) return callback(new Error('Failed to query PTR'));
|
||||
|
||||
debug('checkPtrRecord: Actual:%s Expecting:%s', message.answer[0].data, fqdn);
|
||||
callback(null, message.answer[0].data === fqdn);
|
||||
});
|
||||
|
||||
req.send();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
42
src/docker.js
Normal file
42
src/docker.js
Normal file
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
var Docker = require('dockerode'),
|
||||
fs = require('fs'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
url = require('url');
|
||||
|
||||
exports = module.exports = (function () {
|
||||
var docker;
|
||||
var options = connectOptions(); // the real docker
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// test code runs a docker proxy on this port
|
||||
docker = new Docker({ host: 'http://localhost', port: 5687 });
|
||||
} else {
|
||||
docker = new Docker(options);
|
||||
}
|
||||
|
||||
// proxy code uses this to route to the real docker
|
||||
docker.options = options;
|
||||
|
||||
return docker;
|
||||
})();
|
||||
|
||||
function connectOptions() {
|
||||
if (os.platform() === 'linux') return { socketPath: '/var/run/docker.sock' };
|
||||
|
||||
// boot2docker configuration
|
||||
var DOCKER_CERT_PATH = process.env.DOCKER_CERT_PATH || path.join(process.env.HOME, '.boot2docker/certs/boot2docker-vm');
|
||||
var DOCKER_HOST = process.env.DOCKER_HOST || 'tcp://192.168.59.103:2376';
|
||||
|
||||
return {
|
||||
protocol: 'https',
|
||||
host: url.parse(DOCKER_HOST).hostname,
|
||||
port: url.parse(DOCKER_HOST).port,
|
||||
ca: fs.readFileSync(path.join(DOCKER_CERT_PATH, 'ca.pem')),
|
||||
cert: fs.readFileSync(path.join(DOCKER_CERT_PATH, 'cert.pem')),
|
||||
key: fs.readFileSync(path.join(DOCKER_CERT_PATH, 'key.pem'))
|
||||
};
|
||||
}
|
||||
|
||||
106
src/ldap.js
Normal file
106
src/ldap.js
Normal file
@@ -0,0 +1,106 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
start: start
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:ldap'),
|
||||
user = require('./user.js'),
|
||||
UserError = user.UserError,
|
||||
ldap = require('ldapjs');
|
||||
|
||||
var gServer = null;
|
||||
|
||||
var NOOP = function () {};
|
||||
|
||||
var gLogger = {
|
||||
trace: NOOP,
|
||||
debug: NOOP,
|
||||
info: debug,
|
||||
warn: debug,
|
||||
error: console.error,
|
||||
fatal: console.error
|
||||
};
|
||||
|
||||
function start(callback) {
|
||||
assert(typeof callback === 'function');
|
||||
|
||||
gServer = ldap.createServer({ log: gLogger });
|
||||
|
||||
gServer.search('ou=users,dc=cloudron', function (req, res, next) {
|
||||
debug('ldap user search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
|
||||
|
||||
user.list(function (error, result){
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
// send user objects
|
||||
result.forEach(function (entry) {
|
||||
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
|
||||
|
||||
var tmp = {
|
||||
dn: dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['user'],
|
||||
cn: entry.id,
|
||||
uid: entry.id,
|
||||
mail: entry.email,
|
||||
displayname: entry.username,
|
||||
username: entry.username
|
||||
}
|
||||
};
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
|
||||
res.send(tmp);
|
||||
debug('ldap user send:', tmp);
|
||||
}
|
||||
});
|
||||
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
|
||||
gServer.search('ou=groups,dc=cloudron', function (req, res, next) {
|
||||
debug('ldap group search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
|
||||
|
||||
user.list(function (error, result){
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
// we only have an admin group
|
||||
var dn = ldap.parseDN('cn=admin,ou=groups,dc=cloudron');
|
||||
|
||||
var tmp = {
|
||||
dn: dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['group'],
|
||||
cn: 'admin',
|
||||
memberuid: result.filter(function (entry) { return entry.admin; }).map(function(entry) { return entry.id; })
|
||||
}
|
||||
};
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
|
||||
res.send(tmp);
|
||||
debug('ldap group send:', tmp);
|
||||
}
|
||||
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
|
||||
gServer.bind('dc=cloudron', function(req, res, next) {
|
||||
debug('ldap bind: %s', req.dn.toString());
|
||||
|
||||
if (!req.dn.rdns[0].cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
user.verify(req.dn.rdns[0].cn, req.credentials || '', function (error, result) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error));
|
||||
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
|
||||
gServer.listen(config.get('ldapPort'), callback);
|
||||
}
|
||||
55
src/locker.js
Normal file
55
src/locker.js
Normal file
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:locker'),
|
||||
EventEmitter = require('events').EventEmitter,
|
||||
util = require('util');
|
||||
|
||||
function Locker() {
|
||||
this._operation = null;
|
||||
this._timestamp = null;
|
||||
this._watcherId = -1;
|
||||
}
|
||||
util.inherits(Locker, EventEmitter);
|
||||
|
||||
// these are mutually exclusive operations
|
||||
Locker.prototype.OP_BOX_UPDATE = 'box_update';
|
||||
Locker.prototype.OP_FULL_BACKUP = 'full_backup';
|
||||
Locker.prototype.OP_APPTASK = 'apptask';
|
||||
Locker.prototype.OP_MIGRATE = 'migrate';
|
||||
|
||||
Locker.prototype.lock = function (operation) {
|
||||
assert.strictEqual(typeof operation, 'string');
|
||||
|
||||
if (this._operation !== null) return new Error('Already locked for ' + this._operation);
|
||||
|
||||
this._operation = operation;
|
||||
this._timestamp = new Date();
|
||||
var that = this;
|
||||
this._watcherId = setInterval(function () { debug('Lock unreleased %s', that._operation); }, 1000 * 60 * 5);
|
||||
|
||||
debug('Acquired : %s', this._operation);
|
||||
|
||||
this.emit('locked', this._operation);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
Locker.prototype.unlock = function (operation) {
|
||||
assert.strictEqual(typeof operation, 'string');
|
||||
|
||||
if (this._operation !== operation) throw new Error('Mismatched unlock. Current lock is for ' + this._operation); // throw because this is a programming error
|
||||
|
||||
debug('Released : %s', this._operation);
|
||||
|
||||
this._operation = null;
|
||||
this._timestamp = null;
|
||||
clearInterval(this._watcherId);
|
||||
this._watcherId = -1;
|
||||
|
||||
this.emit('unlocked', operation);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
exports = module.exports = new Locker();
|
||||
19
src/mail_templates/app_down.ejs
Normal file
19
src/mail_templates/app_down.ejs
Normal file
@@ -0,0 +1,19 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear Admin,
|
||||
|
||||
The application titled '<%= title %>' that you installed at <%= appFqdn %>
|
||||
is not responding.
|
||||
|
||||
This is most likely a problem in the application. Please report this issue to
|
||||
support@cloudron.io (by forwarding this email).
|
||||
|
||||
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
|
||||
|
||||
Thank you,
|
||||
Application WatchDog
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
|
||||
15
src/mail_templates/app_update_available.ejs
Normal file
15
src/mail_templates/app_update_available.ejs
Normal file
@@ -0,0 +1,15 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear Admin,
|
||||
|
||||
A new version of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
|
||||
|
||||
Please update at your convenience at <%= webadminUrl %>.
|
||||
|
||||
Thank you,
|
||||
Update Manager
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
|
||||
20
src/mail_templates/box_update_available.ejs
Normal file
20
src/mail_templates/box_update_available.ejs
Normal file
@@ -0,0 +1,20 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear Admin,
|
||||
|
||||
A new version of Cloudron <%= fqdn %> is available!
|
||||
|
||||
Please update at your convenience at <%= webadminUrl %>.
|
||||
|
||||
Changelog:
|
||||
<% for (var i = 0; i < changelog.length; i++) { %>
|
||||
* <%- changelog[i] %>
|
||||
<% } %>
|
||||
|
||||
Thank you,
|
||||
Update Manager
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
|
||||
19
src/mail_templates/crash_notification.ejs
Normal file
19
src/mail_templates/crash_notification.ejs
Normal file
@@ -0,0 +1,19 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear Cloudron Team,
|
||||
|
||||
unfortunately the <%= program %> on <%= fqdn %> crashed unexpectedly!
|
||||
|
||||
Please see some excerpt of the logs below.
|
||||
|
||||
Thank you,
|
||||
Your Cloudron
|
||||
|
||||
-------------------------------------
|
||||
|
||||
<%= context %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
|
||||
20
src/mail_templates/password_reset.ejs
Normal file
20
src/mail_templates/password_reset.ejs
Normal file
@@ -0,0 +1,20 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear <%= username %>,
|
||||
|
||||
Someone, hopefully you, has requested your <%= fqdn %>'s account password
|
||||
be reset. If you did not request this reset, please ignore this message.
|
||||
|
||||
To reset your password, please visit the following page:
|
||||
<%= resetLink %>
|
||||
|
||||
When you visit the above page, you will be prompted to enter a new password.
|
||||
After you have submitted the form, you can login using the new password.
|
||||
|
||||
Thank you,
|
||||
<%= fqdn %> Admin
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
|
||||
15
src/mail_templates/user_event.ejs
Normal file
15
src/mail_templates/user_event.ejs
Normal file
@@ -0,0 +1,15 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear Admin,
|
||||
|
||||
User with name '<%= username %>' (<%= email %>) <%= event %> in the Cloudron at <%= fqdn %>.
|
||||
|
||||
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
|
||||
|
||||
Thank you,
|
||||
User Manager
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
|
||||
24
src/mail_templates/welcome_user.ejs
Normal file
24
src/mail_templates/welcome_user.ejs
Normal file
@@ -0,0 +1,24 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear <%= user.username %>,
|
||||
|
||||
I am excited to welcome you to my Cloudron <%= fqdn %>!
|
||||
|
||||
The Cloudron is our own Private Cloud. You can read more about it
|
||||
at https://www.cloudron.io.
|
||||
|
||||
You username is '<%= user.username %>'
|
||||
|
||||
To get started, create your account by visiting the following page:
|
||||
<%= setupLink %>
|
||||
|
||||
When you visit the above page, you will be prompted to enter a new password.
|
||||
After you have submitted the form, you can login using the new password.
|
||||
|
||||
Thank you,
|
||||
<%= invitor.email %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
|
||||
279
src/mailer.js
Normal file
279
src/mailer.js
Normal file
@@ -0,0 +1,279 @@
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
|
||||
userAdded: userAdded,
|
||||
userRemoved: userRemoved,
|
||||
adminChanged: adminChanged,
|
||||
passwordReset: passwordReset,
|
||||
boxUpdateAvailable: boxUpdateAvailable,
|
||||
appUpdateAvailable: appUpdateAvailable,
|
||||
|
||||
sendCrashNotification: sendCrashNotification,
|
||||
|
||||
appDied: appDied
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:mailer'),
|
||||
digitalocean = require('./digitalocean.js'),
|
||||
docker = require('./docker.js'),
|
||||
ejs = require('ejs'),
|
||||
nodemailer = require('nodemailer'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
smtpTransport = require('nodemailer-smtp-transport'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
userdb = require('./userdb.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates');
|
||||
|
||||
var gMailQueue = [ ],
|
||||
gDnsReady = false,
|
||||
gCheckDnsTimerId = null;
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
checkDns();
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// TODO: interrupt processQueue as well
|
||||
clearTimeout(gCheckDnsTimerId);
|
||||
gCheckDnsTimerId = null;
|
||||
|
||||
debug(gMailQueue.length + ' mail items dropped');
|
||||
gMailQueue = [ ];
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function checkDns() {
|
||||
digitalocean.checkPtrRecord(sysinfo.getIp(), config.fqdn(), function (error, ok) {
|
||||
if (error || !ok) {
|
||||
debug('PTR record not setup yet');
|
||||
gCheckDnsTimerId = setTimeout(checkDns, 10000);
|
||||
return;
|
||||
}
|
||||
|
||||
gDnsReady = true;
|
||||
processQueue();
|
||||
});
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
docker.getContainer('mail').inspect(function (error, data) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
var mailServerIp = safe.query(data, 'NetworkSettings.IPAddress');
|
||||
if (!mailServerIp) return debug('Error querying mail server IP');
|
||||
|
||||
var transport = nodemailer.createTransport(smtpTransport({
|
||||
host: mailServerIp,
|
||||
port: 25
|
||||
}));
|
||||
|
||||
var mailQueueCopy = gMailQueue;
|
||||
gMailQueue = [ ];
|
||||
|
||||
debug('Processing mail queue of size %d', mailQueueCopy.length);
|
||||
|
||||
async.mapSeries(mailQueueCopy, function iterator(mailOptions, callback) {
|
||||
transport.sendMail(mailOptions, function (error) {
|
||||
if (error) return console.error(error); // TODO: requeue?
|
||||
debug('Email sent to ' + mailOptions.to);
|
||||
});
|
||||
callback(null);
|
||||
}, function done() {
|
||||
debug('Done processing mail queue');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function enqueue(mailOptions) {
|
||||
assert.strictEqual(typeof mailOptions, 'object');
|
||||
|
||||
debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to);
|
||||
gMailQueue.push(mailOptions);
|
||||
|
||||
if (gDnsReady) processQueue();
|
||||
}
|
||||
|
||||
function render(templateFile, params) {
|
||||
assert.strictEqual(typeof templateFile, 'string');
|
||||
assert.strictEqual(typeof params, 'object');
|
||||
|
||||
return ejs.render(safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8'), params);
|
||||
}
|
||||
|
||||
function getAdminEmails(callback) {
|
||||
userdb.getAllAdmins(function (error, admins) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var adminEmails = [ ];
|
||||
admins.forEach(function (admin) { adminEmails.push(admin.email); });
|
||||
|
||||
callback(null, adminEmails);
|
||||
});
|
||||
}
|
||||
|
||||
function mailUserEventToAdmins(user, event) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof event, 'string');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
|
||||
adminEmails = _.difference(adminEmails, [ user.email ]);
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('%s %s in Cloudron %s', user.username, event, config.fqdn()),
|
||||
text: render('user_event.ejs', { fqdn: config.fqdn(), username: user.username, email: user.email, event: event, format: 'text' }),
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function userAdded(user, invitor) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert(typeof invitor === 'object');
|
||||
|
||||
debug('Sending mail for userAdded');
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
webadminUrl: config.adminOrigin(),
|
||||
setupLink: config.adminOrigin() + '/api/v1/session/password/setup.html?reset_token=' + user.resetToken,
|
||||
format: 'text',
|
||||
fqdn: config.fqdn(),
|
||||
invitor: invitor
|
||||
};
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: user.email,
|
||||
subject: util.format('Welcome to Cloudron %s', config.fqdn()),
|
||||
text: render('welcome_user.ejs', templateData)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
|
||||
mailUserEventToAdmins(user, 'was added');
|
||||
}
|
||||
|
||||
function userRemoved(username) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
|
||||
debug('Sending mail for userRemoved');
|
||||
|
||||
mailUserEventToAdmins({ username: username }, 'was removed');
|
||||
}
|
||||
|
||||
function adminChanged(user) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
|
||||
debug('Sending mail for adminChanged');
|
||||
|
||||
mailUserEventToAdmins(user, user.admin ? 'is now an admin' : 'is no more an admin');
|
||||
}
|
||||
|
||||
function passwordReset(user) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
|
||||
debug('Sending mail for password reset for user %s.', user.username);
|
||||
|
||||
var resetLink = config.adminOrigin() + '/api/v1/session/password/reset.html?reset_token=' + user.resetToken;
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: user.email,
|
||||
subject: 'Password Reset Request',
|
||||
text: render('password_reset.ejs', { fqdn: config.fqdn(), username: user.username, resetLink: resetLink, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
}
|
||||
|
||||
function appDied(app) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
|
||||
debug('Sending mail for app %s @ %s died', app.id, app.location);
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('App %s is down', app.location),
|
||||
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: config.appFqdn(app.location), format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function boxUpdateAvailable(newBoxVersion, changelog) {
|
||||
assert.strictEqual(typeof newBoxVersion, 'string');
|
||||
assert(util.isArray(changelog));
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('%s has a new update available', config.fqdn()),
|
||||
text: render('box_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), newBoxVersion: newBoxVersion, changelog: changelog, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function appUpdateAvailable(app, updateInfo) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof updateInfo, 'object');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('%s has a new update available', app.fqdn),
|
||||
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function sendCrashNotification(program, context) {
|
||||
assert.strictEqual(typeof program, 'string');
|
||||
assert.strictEqual(typeof context, 'string');
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: 'admin@cloudron.io',
|
||||
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
|
||||
text: render('crash_notification.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
}
|
||||
8
src/middleware/contentType.js
Normal file
8
src/middleware/contentType.js
Normal file
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function contentType(type) {
|
||||
return function (req, res, next) {
|
||||
res.setHeader('Content-Type', type);
|
||||
next();
|
||||
};
|
||||
};
|
||||
55
src/middleware/cors.js
Normal file
55
src/middleware/cors.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/* jshint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var url = require('url');
|
||||
|
||||
/*
|
||||
* CORS middleware
|
||||
*
|
||||
* options can contains a list of origins
|
||||
*/
|
||||
module.exports = function cors(options) {
|
||||
options = options || { };
|
||||
var maxAge = options.maxAge || 60 * 60 * 25 * 5; // 5 days
|
||||
var origins = options.origins || [ '*' ];
|
||||
var allowCredentials = options.allowCredentials || false; // cookies
|
||||
|
||||
return function (req, res, next) {
|
||||
var requestOrigin = req.headers.origin;
|
||||
if (!requestOrigin) return next();
|
||||
|
||||
requestOrigin = url.parse(requestOrigin);
|
||||
|
||||
var hostname = requestOrigin.host.split(':')[0]; // remove any port
|
||||
var originAllowed = origins.some(function (o) { return o === '*' || o === hostname; });
|
||||
if (!originAllowed) {
|
||||
return res.status(405).send('CORS not allowed from this domain');
|
||||
}
|
||||
|
||||
// respond back with req.headers.origin which might contain the scheme
|
||||
res.header('Access-Control-Allow-Origin', req.headers.origin);
|
||||
res.header('Access-Control-Allow-Credentials', allowCredentials);
|
||||
|
||||
// handle preflighted requests
|
||||
if (req.method === 'OPTIONS') {
|
||||
if (req.headers['access-control-request-method']) {
|
||||
res.header('Access-Control-Allow-Methods', 'GET, PUT, DELETE, POST, OPTIONS');
|
||||
}
|
||||
|
||||
if (req.headers['access-control-request-headers']) {
|
||||
res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers']);
|
||||
}
|
||||
|
||||
res.header('Access-Control-Max-Age', maxAge);
|
||||
|
||||
return res.status(200).send();
|
||||
}
|
||||
|
||||
if (req.headers['access-control-request-headers']) {
|
||||
res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers']);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
17
src/middleware/index.js
Normal file
17
src/middleware/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
contentType: require('./contentType'),
|
||||
cookieParser: require('cookie-parser'),
|
||||
cors: require('./cors'),
|
||||
csrf: require('csurf'),
|
||||
favicon: require('serve-favicon'),
|
||||
json: require('body-parser').json,
|
||||
morgan: require('morgan'),
|
||||
proxy: require('proxy-middleware'),
|
||||
lastMile: require('connect-lastmile'),
|
||||
multipart: require('./multipart.js'),
|
||||
session: require('express-session'),
|
||||
timeout: require('connect-timeout'),
|
||||
urlencoded: require('body-parser').urlencoded
|
||||
};
|
||||
47
src/middleware/multipart.js
Normal file
47
src/middleware/multipart.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/* jshint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var multiparty = require('multiparty'),
|
||||
timeout = require('connect-timeout');
|
||||
|
||||
function _mime(req) {
|
||||
var str = req.headers['content-type'] || '';
|
||||
return str.split(';')[0];
|
||||
}
|
||||
|
||||
module.exports = function multipart(options) {
|
||||
return function (req, res, next) {
|
||||
if (_mime(req) !== 'multipart/form-data') return res.status(400).send('Invalid content-type. Expecting multipart');
|
||||
|
||||
var form = new multiparty.Form({
|
||||
uploadDir: '/tmp',
|
||||
keepExtensions: true,
|
||||
maxFieldsSize: options.maxFieldsSize || (2 * 1024), // only field size, not files
|
||||
limit: options.limit || '8mb', // file sizes
|
||||
autoFiles: true
|
||||
});
|
||||
|
||||
// increase timeout of file uploads by default to 3 mins
|
||||
if (req.clearTimeout) req.clearTimeout(); // clear any previous installed timeout middleware
|
||||
|
||||
timeout(options.timeout || (3 * 60 * 1000))(req, res, function () {
|
||||
req.fields = { };
|
||||
req.files = { };
|
||||
|
||||
form.parse(req, function (err, fields, files) {
|
||||
if (err) return res.status(400).send('Error parsing request');
|
||||
next(null);
|
||||
});
|
||||
|
||||
form.on('file', function (name, file) {
|
||||
req.files[name] = file;
|
||||
});
|
||||
|
||||
form.on('field', function (name, value) {
|
||||
req.fields[name] = value; // otherwise fields.name is an array
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
47
src/oauth2views/callback.ejs
Normal file
47
src/oauth2views/callback.ejs
Normal file
@@ -0,0 +1,47 @@
|
||||
<% include header %>
|
||||
|
||||
<script>
|
||||
|
||||
'use strict';
|
||||
|
||||
var code = null;
|
||||
var redirectURI = null;
|
||||
var accessToken = null;
|
||||
var tokenType = null;
|
||||
var state = null;
|
||||
|
||||
var args = window.location.search.slice(1).split('&');
|
||||
args.forEach(function (arg) {
|
||||
var tmp = arg.split('=');
|
||||
if (tmp[0] === 'redirectURI') {
|
||||
redirectURI = window.decodeURIComponent(tmp[1]);
|
||||
} else if (tmp[0] === 'code') {
|
||||
code = window.decodeURIComponent(tmp[1]);
|
||||
} else if (tmp[0] === 'state') {
|
||||
state = window.decodeURIComponent(tmp[1]);
|
||||
}
|
||||
});
|
||||
|
||||
args = window.location.hash.slice(1).split('&');
|
||||
args.forEach(function (arg) {
|
||||
var tmp = arg.split('=');
|
||||
if (tmp[0] === 'access_token') {
|
||||
accessToken = window.decodeURIComponent(tmp[1]);
|
||||
} else if (tmp[0] === 'token_type') {
|
||||
tokenType = window.decodeURIComponent(tmp[1]);
|
||||
} else if (tmp[0] === 'state') {
|
||||
state = window.decodeURIComponent(tmp[1]);
|
||||
}
|
||||
});
|
||||
|
||||
if (code && redirectURI) {
|
||||
window.location.href = redirectURI + '?code=' + code + (state ? '&state=' + state : '');
|
||||
} else if (redirectURI && accessToken) {
|
||||
window.location.href = redirectURI + '?token=' + accessToken + (state ? '&state=' + state : '');
|
||||
} else {
|
||||
window.location.href = '/api/v1/session/login';
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<% include footer %>;
|
||||
38
src/oauth2views/dialog.ejs
Normal file
38
src/oauth2views/dialog.ejs
Normal file
@@ -0,0 +1,38 @@
|
||||
<% include header %>
|
||||
|
||||
<form action="/api/v1/oauth/dialog/authorize/decision" method="post">
|
||||
<input name="transaction_id" type="hidden" value="<%= transactionID %>">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3"></div>
|
||||
<div class="col-md-6">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
Hi <%= user.username %>!
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<b><%= client.name %></b> is requesting access to your account.
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
Do you approve?
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<input class="btn btn-danger btn-outline" type="submit" value="Deny" name="cancel" id="deny"/>
|
||||
<input class="btn btn-success btn-outline" type="submit" value="Allow"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3"></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<% include footer %>
|
||||
24
src/oauth2views/error.ejs
Normal file
24
src/oauth2views/error.ejs
Normal file
@@ -0,0 +1,24 @@
|
||||
<% include header %>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8">
|
||||
<div class="alert alert-danger">
|
||||
<%- message %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8 text-center">
|
||||
<a href="<%- adminOrigin %>">Back</a>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
3
src/oauth2views/footer.ejs
Normal file
3
src/oauth2views/footer.ejs
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
</body>
|
||||
</html>
|
||||
28
src/oauth2views/header.ejs
Normal file
28
src/oauth2views/header.ejs
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title> Cloudron Login </title>
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- jQuery-->
|
||||
<script src="<%= adminOrigin %>/3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script src="<%= adminOrigin %>/3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script src="<%= adminOrigin %>/3rdparty/js/angular.min.js"></script>
|
||||
<script src="<%= adminOrigin %>/3rdparty/js/angular-loader.min.js"></script>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="<%= adminOrigin %>/theme.css" rel="stylesheet">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
44
src/oauth2views/login.ejs
Normal file
44
src/oauth2views/login.ejs
Normal file
@@ -0,0 +1,44 @@
|
||||
<% include header %>
|
||||
|
||||
<center>
|
||||
<h1>Login to <%= applicationName %></h1>
|
||||
</center>
|
||||
|
||||
<% if (error) { %>
|
||||
<center>
|
||||
<br/><br/>
|
||||
<h4 class="has-error"><%= error %></h4>
|
||||
</center>
|
||||
<% } %>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form id="loginForm" action="" method="post">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputUsername">Username or Email</label>
|
||||
<input type="text" class="form-control" id="inputUsername" name="username" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" name="password" id="inputPassword" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
|
||||
</form>
|
||||
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
(function () {
|
||||
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
document.getElementById('loginForm').action = '/api/v1/session/login?returnTo=' + search.returnTo;
|
||||
})();
|
||||
|
||||
</script>
|
||||
|
||||
<% include footer %>
|
||||
40
src/oauth2views/password_reset.ejs
Normal file
40
src/oauth2views/password_reset.ejs
Normal file
@@ -0,0 +1,40 @@
|
||||
<% include header %>
|
||||
|
||||
<!-- tester -->
|
||||
|
||||
<script>
|
||||
|
||||
'use strict';
|
||||
|
||||
// very basic angular app
|
||||
var app = angular.module('Application', []);
|
||||
app.controller('Controller', [function () {}]);
|
||||
|
||||
</script>
|
||||
|
||||
<center>
|
||||
<h1>Hello <%= user.username %> create a new password</h1>
|
||||
</center>
|
||||
|
||||
<div class="container" ng-app="Application" ng-controller="Controller">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/password/reset" method="post" name="resetForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-maxlength="512" ng-minlength="5" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (resetForm.passwordRepeat.$invalid || password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
25
src/oauth2views/password_reset_request.ejs
Normal file
25
src/oauth2views/password_reset_request.ejs
Normal file
@@ -0,0 +1,25 @@
|
||||
<% include header %>
|
||||
|
||||
<!-- tester -->
|
||||
|
||||
<center>
|
||||
<h1>Reset your password</h1>
|
||||
</center>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/password/resetRequest" method="post" autocomplete="off">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputIdentifier">Username or Email</label>
|
||||
<input type="text" class="form-control" id="inputIdentifier" name="identifier" autofocus required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Reset"/>
|
||||
</form>
|
||||
<a href="/api/v1/session/login">Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
18
src/oauth2views/password_reset_sent.ejs
Normal file
18
src/oauth2views/password_reset_sent.ejs
Normal file
@@ -0,0 +1,18 @@
|
||||
<% include header %>
|
||||
|
||||
<!-- tester -->
|
||||
|
||||
<center>
|
||||
<h1>Reset your password successful</h1>
|
||||
</center>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<p>An email was sent to you with a link to create a new password.</p>
|
||||
If you have not received any email after some time, maybe you have misspelled your email address, simply try again <a href="/api/v1/session/password/resetRequest.html">here</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
40
src/oauth2views/password_setup.ejs
Normal file
40
src/oauth2views/password_setup.ejs
Normal file
@@ -0,0 +1,40 @@
|
||||
<% include header %>
|
||||
|
||||
<!-- tester -->
|
||||
|
||||
<script>
|
||||
|
||||
'use strict';
|
||||
|
||||
// very basic angular app
|
||||
var app = angular.module('Application', []);
|
||||
app.controller('Controller', [function () {}]);
|
||||
|
||||
</script>
|
||||
|
||||
<center>
|
||||
<h1>Hello <%= user.username %> create a password</h1>
|
||||
</center>
|
||||
|
||||
<div class="container" ng-app="Application" ng-controller="Controller">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/password/reset" method="post" name="setupForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': setupForm.password.$dirty && setupForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-maxlength="512" ng-minlength="5" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': setupForm.passwordRepeat.$dirty && (setupForm.passwordRepeat.$invalid || password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
28
src/paths.js
Normal file
28
src/paths.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var config = require('./config.js'),
|
||||
path = require('path');
|
||||
|
||||
// keep these values in sync with start.sh
|
||||
exports = module.exports = {
|
||||
NGINX_CONFIG_DIR: path.join(config.baseDir(), 'data/nginx'),
|
||||
NGINX_APPCONFIG_DIR: path.join(config.baseDir(), 'data/nginx/applications'),
|
||||
NGINX_CERT_DIR: path.join(config.baseDir(), 'data/nginx/cert'),
|
||||
|
||||
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'data/addons'),
|
||||
|
||||
COLLECTD_APPCONFIG_DIR: path.join(config.baseDir(), 'data/collectd/collectd.conf.d'),
|
||||
|
||||
DATA_DIR: path.join(config.baseDir(), 'data'),
|
||||
BOX_DATA_DIR: path.join(config.baseDir(), 'data/box'),
|
||||
// this is not part of appdata because an icon may be set before install
|
||||
APPICONS_DIR: path.join(config.baseDir(), 'data/box/appicons'),
|
||||
MAIL_DATA_DIR: path.join(config.baseDir(), 'data/box/mail'),
|
||||
|
||||
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'),
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
|
||||
FAVICON_FILE: path.join(__dirname + '/../assets/favicon.ico')
|
||||
};
|
||||
47
src/progress.js
Normal file
47
src/progress.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
set: set,
|
||||
clear: clear,
|
||||
get: get,
|
||||
|
||||
UPDATE: 'update',
|
||||
BACKUP: 'backup'
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:progress');
|
||||
|
||||
// if progress.update or progress.backup are object, they will contain 'percent' and 'message' properties
|
||||
// otherwise no such operation is currently ongoing
|
||||
var progress = {
|
||||
update: null,
|
||||
backup: null
|
||||
};
|
||||
|
||||
function set(tag, percent, message) {
|
||||
assert(tag === exports.UPDATE || tag === exports.BACKUP);
|
||||
assert.strictEqual(typeof percent, 'number');
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
progress[tag] = {
|
||||
percent: percent,
|
||||
message: message
|
||||
};
|
||||
|
||||
debug('%s: %s %s', tag, percent, message);
|
||||
}
|
||||
|
||||
function clear(tag) {
|
||||
assert(tag === exports.UPDATE || tag === exports.BACKUP);
|
||||
|
||||
progress[tag] = null;
|
||||
|
||||
debug('clearing %s', tag);
|
||||
}
|
||||
|
||||
function get() {
|
||||
return progress;
|
||||
}
|
||||
356
src/routes/apps.js
Normal file
356
src/routes/apps.js
Normal file
@@ -0,0 +1,356 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getApp: getApp,
|
||||
getAppBySubdomain: getAppBySubdomain,
|
||||
getApps: getApps,
|
||||
getAppIcon: getAppIcon,
|
||||
installApp: installApp,
|
||||
configureApp: configureApp,
|
||||
uninstallApp: uninstallApp,
|
||||
restoreApp: restoreApp,
|
||||
backupApp: backupApp,
|
||||
updateApp: updateApp,
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream,
|
||||
|
||||
stopApp: stopApp,
|
||||
startApp: startApp,
|
||||
exec: exec
|
||||
};
|
||||
|
||||
var apps = require('../apps.js'),
|
||||
AppsError = apps.AppsError,
|
||||
assert = require('assert'),
|
||||
debug = require('debug')('box:routes/apps'),
|
||||
fs = require('fs'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid');
|
||||
|
||||
function removeInternalAppFields(app) {
|
||||
return {
|
||||
id: app.id,
|
||||
appStoreId: app.appStoreId,
|
||||
installationState: app.installationState,
|
||||
installationProgress: app.installationProgress,
|
||||
runState: app.runState,
|
||||
health: app.health,
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
lastBackupId: app.lastBackupId,
|
||||
manifest: app.manifest,
|
||||
portBindings: app.portBindings,
|
||||
iconUrl: app.iconUrl,
|
||||
fqdn: app.fqdn
|
||||
};
|
||||
}
|
||||
|
||||
function getApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
apps.get(req.params.id, function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, removeInternalAppFields(app)));
|
||||
});
|
||||
}
|
||||
|
||||
function getAppBySubdomain(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.subdomain, 'string');
|
||||
|
||||
apps.getBySubdomain(req.params.subdomain, function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such subdomain'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, removeInternalAppFields(app)));
|
||||
});
|
||||
}
|
||||
|
||||
function getApps(req, res, next) {
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
allApps = allApps.map(removeInternalAppFields);
|
||||
|
||||
next(new HttpSuccess(200, { apps: allApps }));
|
||||
});
|
||||
}
|
||||
|
||||
function getAppIcon(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
var iconPath = paths.APPICONS_DIR + '/' + req.params.id + '.png';
|
||||
fs.exists(iconPath, function (exists) {
|
||||
if (!exists) return next(new HttpError(404, 'No such icon'));
|
||||
res.sendFile(iconPath);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Installs an app
|
||||
* @bodyparam {string} appStoreId The id of the app to be installed
|
||||
* @bodyparam {manifest} manifest The app manifest
|
||||
* @bodyparam {string} password The user's password
|
||||
* @bodyparam {string} location The subdomain where the app is to be installed
|
||||
* @bodyparam {object} portBindings map from environment variable name to (public) host port. can be null.
|
||||
If a value in manifest.tcpPorts is missing in portBindings, the port/service is disabled
|
||||
* @bodyparam {icon} icon Base64 encoded image
|
||||
*/
|
||||
function installApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
var data = req.body;
|
||||
|
||||
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
|
||||
if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest is required'));
|
||||
if (typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId is required'));
|
||||
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
|
||||
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
if (typeof data.accessRestriction !== 'string') return next(new HttpError(400, 'accessRestriction is required'));
|
||||
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
|
||||
|
||||
// allow tests to provide an appId for testing
|
||||
var appId = (process.env.NODE_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
|
||||
|
||||
debug('Installing app id:%s storeid:%s loc:%s port:%j restrict:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.manifest);
|
||||
|
||||
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, function (error) {
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { id: appId } ));
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Configure an app
|
||||
* @bodyparam {string} password The user's password
|
||||
* @bodyparam {string} location The subdomain where the app is to be installed
|
||||
* @bodyparam {object} portBindings map from env to (public) host port. can be null.
|
||||
If a value in manifest.tcpPorts is missing in portBindings, the port/service is disabled
|
||||
*/
|
||||
function configureApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
var data = req.body;
|
||||
|
||||
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
|
||||
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
|
||||
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
if (typeof data.accessRestriction !== 'string') return next(new HttpError(400, 'accessRestriction is required'));
|
||||
|
||||
debug('Configuring app id:%s location:%s bindings:%j', req.params.id, data.location, data.portBindings);
|
||||
|
||||
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, function (error) {
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
});
|
||||
}
|
||||
|
||||
function restoreApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Restore app id:%s', req.params.id);
|
||||
|
||||
apps.restore(req.params.id, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
});
|
||||
}
|
||||
|
||||
function backupApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Backup app id:%s', req.params.id);
|
||||
|
||||
apps.backup(req.params.id, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(503, error));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Uninstalls an app
|
||||
* @bodyparam {string} id The id of the app to be uninstalled
|
||||
*/
|
||||
function uninstallApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Uninstalling app id:%s', req.params.id);
|
||||
|
||||
apps.uninstall(req.params.id, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
});
|
||||
}
|
||||
|
||||
function startApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Start app id:%s', req.params.id);
|
||||
|
||||
apps.start(req.params.id, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
});
|
||||
}
|
||||
|
||||
function stopApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Stop app id:%s', req.params.id);
|
||||
|
||||
apps.stop(req.params.id, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
});
|
||||
}
|
||||
|
||||
function updateApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
var data = req.body;
|
||||
|
||||
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
|
||||
if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest is required'));
|
||||
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
|
||||
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
|
||||
|
||||
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
|
||||
|
||||
apps.update(req.params.id, data.force || false, data.manifest, data.portBindings, data.icon, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
});
|
||||
}
|
||||
|
||||
// this route is for streaming logs
|
||||
function getLogStream(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Getting logstream of app id:%s', req.params.id);
|
||||
|
||||
var fromLine = req.query.fromLine ? parseInt(req.query.fromLine, 10) : -10; // we ignore last-event-id
|
||||
if (isNaN(fromLine)) return next(new HttpError(400, 'fromLine must be a valid number'));
|
||||
|
||||
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
|
||||
|
||||
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
|
||||
|
||||
apps.getLogStream(req.params.id, fromLine, function (error, logStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no', // disable nginx buffering
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
res.write('retry: 3000\n');
|
||||
res.on('close', logStream.close);
|
||||
logStream.on('data', function (data) {
|
||||
var obj = JSON.parse(data);
|
||||
res.write(sse(obj.lineNumber, JSON.stringify(obj)));
|
||||
});
|
||||
logStream.on('end', res.end.bind(res));
|
||||
logStream.on('error', res.end.bind(res, null));
|
||||
});
|
||||
}
|
||||
|
||||
function getLogs(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Getting logs of app id:%s', req.params.id);
|
||||
|
||||
apps.getLogs(req.params.id, function (error, logStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/x-logs',
|
||||
'Content-Disposition': 'attachment; filename="log.txt"',
|
||||
'Cache-Control': 'no-cache',
|
||||
'X-Accel-Buffering': 'no' // disable nginx buffering
|
||||
});
|
||||
logStream.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
function exec(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Execing into app id:%s', req.params.id);
|
||||
|
||||
var cmd = null;
|
||||
if (req.query.cmd) {
|
||||
cmd = safe.JSON.parse(req.query.cmd);
|
||||
if (!util.isArray(cmd) && cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1'));
|
||||
}
|
||||
|
||||
var columns = req.query.columns ? parseInt(req.query.columns, 10) : null;
|
||||
if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number'));
|
||||
|
||||
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
|
||||
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
|
||||
|
||||
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns }, function (error, duplexStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
|
||||
|
||||
req.clearTimeout();
|
||||
res.sendUpgradeHandshake();
|
||||
|
||||
duplexStream.pipe(res.socket);
|
||||
res.socket.pipe(duplexStream);
|
||||
});
|
||||
}
|
||||
|
||||
36
src/routes/backups.js
Normal file
36
src/routes/backups.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
create: create
|
||||
};
|
||||
|
||||
var backups = require('../backups.js'),
|
||||
BackupsError = require('../backups.js').BackupsError,
|
||||
cloudron = require('../cloudron.js'),
|
||||
CloudronError = require('../cloudron.js').CloudronError,
|
||||
debug = require('debug')('box:routes/backups'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function get(req, res, next) {
|
||||
backups.getAllPaged(1, 5, function (error, result) {
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { backups: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function create(req, res, next) {
|
||||
// note that cloudron.backup only waits for backup initiation and not for backup to complete
|
||||
// backup progress can be checked up ny polling the progress api call
|
||||
cloudron.backup(function (error) {
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
105
src/routes/clients.js
Normal file
105
src/routes/clients.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
get: get,
|
||||
update: update,
|
||||
del: del,
|
||||
getAllByUserId: getAllByUserId,
|
||||
getClientTokens: getClientTokens,
|
||||
delClientTokens: delClientTokens
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
validUrl = require('valid-url'),
|
||||
clients = require('../clients.js'),
|
||||
ClientsError = clients.ClientsError,
|
||||
DatabaseError = require('../databaseerror.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function add(req, res, next) {
|
||||
var data = req.body;
|
||||
|
||||
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
|
||||
if (typeof data.appId !== 'string' || !data.appId) return next(new HttpError(400, 'appId is required'));
|
||||
if (typeof data.redirectURI !== 'string' || !data.redirectURI) return next(new HttpError(400, 'redirectURI is required'));
|
||||
if (typeof data.scope !== 'string' || !data.scope) return next(new HttpError(400, 'scope is required'));
|
||||
if (!validUrl.isWebUri(data.redirectURI)) return next(new HttpError(400, 'redirectURI must be a valid uri'));
|
||||
|
||||
// prefix as this route only allows external apps for developers
|
||||
var appId = 'external-' + data.appId;
|
||||
|
||||
clients.add(appId, data.redirectURI, data.scope, function (error, result) {
|
||||
if (error && error.reason === ClientsError.INVALID_SCOPE) return next(new HttpError(400, 'Invalid scope'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(201, result));
|
||||
});
|
||||
}
|
||||
|
||||
function get(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.clientId, 'string');
|
||||
|
||||
clients.get(req.params.clientId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(404, 'No such client'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, result));
|
||||
});
|
||||
}
|
||||
|
||||
function update(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.clientId, 'string');
|
||||
|
||||
var data = req.body;
|
||||
|
||||
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
|
||||
if (typeof data.appId !== 'string' || !data.appId) return next(new HttpError(400, 'appId is required'));
|
||||
if (typeof data.redirectURI !== 'string' || !data.redirectURI) return next(new HttpError(400, 'redirectURI is required'));
|
||||
if (!validUrl.isWebUri(data.redirectURI)) return next(new HttpError(400, 'redirectURI must be a valid uri'));
|
||||
|
||||
clients.update(req.params.clientId, data.appId, data.redirectURI, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(202, result));
|
||||
});
|
||||
}
|
||||
|
||||
function del(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.clientId, 'string');
|
||||
|
||||
clients.del(req.params.clientId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(404, 'no such client'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(204, result));
|
||||
});
|
||||
}
|
||||
|
||||
function getAllByUserId(req, res, next) {
|
||||
clients.getAllWithDetailsByUserId(req.user.id, function (error, result) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, { clients: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function getClientTokens(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.clientId, 'string');
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
clients.getClientTokensByUserId(req.params.clientId, req.user.id, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(404, 'no such client'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, { tokens: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function delClientTokens(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.clientId, 'string');
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
clients.delClientTokensByUserId(req.params.clientId, req.user.id, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(404, 'no such client'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
159
src/routes/cloudron.js
Normal file
159
src/routes/cloudron.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
activate: activate,
|
||||
setupTokenAuth: setupTokenAuth,
|
||||
getStatus: getStatus,
|
||||
reboot: reboot,
|
||||
getProgress: getProgress,
|
||||
getConfig: getConfig,
|
||||
update: update,
|
||||
migrate: migrate,
|
||||
setCertificate: setCertificate
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
cloudron = require('../cloudron.js'),
|
||||
config = require('../config.js'),
|
||||
progress = require('../progress.js'),
|
||||
CloudronError = cloudron.CloudronError,
|
||||
debug = require('debug')('box:routes/cloudron'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
superagent = require('superagent'),
|
||||
safe = require('safetydance'),
|
||||
updateChecker = require('../updatechecker.js');
|
||||
|
||||
/**
|
||||
* Creating an admin user and activate the cloudron.
|
||||
*
|
||||
* @apiParam {string} username The administrator's user name
|
||||
* @apiParam {string} password The administrator's password
|
||||
* @apiParam {string} email The administrator's email address
|
||||
*
|
||||
* @apiSuccess (Created 201) {string} token A valid access token
|
||||
*/
|
||||
function activate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.query.setupToken, 'string');
|
||||
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
|
||||
|
||||
var username = req.body.username;
|
||||
var password = req.body.password;
|
||||
var email = req.body.email;
|
||||
var name = req.body.name || null;
|
||||
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
debug('activate: username:%s ip:%s', username, ip);
|
||||
|
||||
cloudron.activate(username, password, email, name, ip, function (error, info) {
|
||||
if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
|
||||
if (error && error.reason === CloudronError.BAD_USERNAME) return next(new HttpError(400, 'Bad username'));
|
||||
if (error && error.reason === CloudronError.BAD_PASSWORD) return next(new HttpError(400, 'Bad password'));
|
||||
if (error && error.reason === CloudronError.BAD_EMAIL) return next(new HttpError(400, 'Bad email'));
|
||||
if (error && error.reason === CloudronError.BAD_NAME) return next(new HttpError(400, 'Bad name'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
// Now let the api server know we got activated
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken:req.query.setupToken }).end(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 201) return next(new HttpError(500, result.text ? result.text.message : 'Internal error'));
|
||||
|
||||
next(new HttpSuccess(201, info));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupTokenAuth(req, res, next) {
|
||||
assert.strictEqual(typeof req.query, 'object');
|
||||
|
||||
if (typeof req.query.setupToken !== 'string') return next(new HttpError(400, 'no setupToken provided'));
|
||||
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken }).end(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 200) return next(new HttpError(500, result.text ? result.text.message : 'Internal error'));
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(req, res, next) {
|
||||
cloudron.getStatus(function (error, status) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, status));
|
||||
});
|
||||
}
|
||||
|
||||
function getProgress(req, res, next) {
|
||||
return next(new HttpSuccess(200, progress.get()));
|
||||
}
|
||||
|
||||
function reboot(req, res, next) {
|
||||
// Finish the request, to let the appstore know we triggered the restore it
|
||||
next(new HttpSuccess(202, {}));
|
||||
|
||||
cloudron.reboot();
|
||||
}
|
||||
|
||||
function getConfig(req, res, next) {
|
||||
cloudron.getConfig(function (error, cloudronConfig) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, cloudronConfig));
|
||||
});
|
||||
}
|
||||
|
||||
function update(req, res, next) {
|
||||
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
|
||||
if (!boxUpdateInfo) return next(new HttpError(422, 'No update available'));
|
||||
|
||||
// this only initiates the update, progress can be checked via the progress route
|
||||
cloudron.update(boxUpdateInfo, function (error) {
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function migrate(req, res, next) {
|
||||
if (typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
|
||||
if (typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
|
||||
|
||||
debug('Migration requested', req.body.size, req.body.region);
|
||||
|
||||
cloudron.migrate(req.body.size, req.body.region, function (error) {
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function setCertificate(req, res, next) {
|
||||
assert.strictEqual(typeof req.files, 'object');
|
||||
|
||||
if (!req.files.certificate) return next(new HttpError(400, 'certificate must be provided'));
|
||||
var certificate = safe.fs.readFileSync(req.files.certificate.path, 'utf8');
|
||||
|
||||
if (!req.files.key) return next(new HttpError(400, 'key must be provided'));
|
||||
var key = safe.fs.readFileSync(req.files.key.path, 'utf8');
|
||||
|
||||
cloudron.setCertificate(certificate, key, function (error) {
|
||||
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
48
src/routes/developer.js
Normal file
48
src/routes/developer.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
enabled: enabled,
|
||||
setEnabled: setEnabled,
|
||||
status: status,
|
||||
login: login
|
||||
};
|
||||
|
||||
var developer = require('../developer.js'),
|
||||
passport = require('passport'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function enabled(req, res, next) {
|
||||
developer.enabled(function (error, enabled) {
|
||||
if (enabled) return next();
|
||||
next(new HttpError(412, 'Developer mode not enabled'));
|
||||
});
|
||||
}
|
||||
|
||||
function setEnabled(req, res, next) {
|
||||
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled must be boolean'));
|
||||
|
||||
developer.setEnabled(req.body.enabled, function (error) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function status(req, res, next) {
|
||||
next(new HttpSuccess(200, {}));
|
||||
}
|
||||
|
||||
function login(req, res, next) {
|
||||
passport.authenticate('local', function (error, user) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (!user) return next(new HttpError(401, 'Invalid credentials'));
|
||||
|
||||
developer.issueDeveloperToken(user, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { token: result.token, expiresAt: result.expiresAt }));
|
||||
});
|
||||
})(req, res, next);
|
||||
}
|
||||
21
src/routes/graphs.js
Normal file
21
src/routes/graphs.js
Normal file
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getGraphs: getGraphs
|
||||
};
|
||||
|
||||
var middleware = require('../middleware/index.js'),
|
||||
url = require('url');
|
||||
|
||||
var graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8000'));
|
||||
|
||||
function getGraphs(req, res, next) {
|
||||
var parsedUrl = url.parse(req.url, true /* parseQueryString */);
|
||||
delete parsedUrl.query['access_token'];
|
||||
delete req.headers['authorization'];
|
||||
delete req.headers['cookies'];
|
||||
req.url = url.format({ pathname: 'render', query: parsedUrl.query });
|
||||
|
||||
graphiteProxy(req, res, next);
|
||||
}
|
||||
|
||||
15
src/routes/index.js
Normal file
15
src/routes/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
apps: require('./apps.js'),
|
||||
cloudron: require('./cloudron.js'),
|
||||
developer: require('./developer.js'),
|
||||
graphs: require('./graphs.js'),
|
||||
oauth2: require('./oauth2.js'),
|
||||
settings: require('./settings.js'),
|
||||
clients: require('./clients.js'),
|
||||
backups: require('./backups.js'),
|
||||
internal: require('./internal.js'),
|
||||
user: require('./user.js')
|
||||
};
|
||||
|
||||
23
src/routes/internal.js
Normal file
23
src/routes/internal.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
backup: backup
|
||||
};
|
||||
|
||||
var cloudron = require('../cloudron.js'),
|
||||
debug = require('debug')('box:routes/internal'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function backup(req, res, next) {
|
||||
debug('trigger backup');
|
||||
|
||||
cloudron.backup(function (error) {
|
||||
if (error) debug('Internal route backup failed', error);
|
||||
});
|
||||
|
||||
// we always succeed to trigger a backup
|
||||
next(new HttpSuccess(202, {}));
|
||||
}
|
||||
503
src/routes/oauth2.js
Normal file
503
src/routes/oauth2.js
Normal file
@@ -0,0 +1,503 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
authcodedb = require('../authcodedb'),
|
||||
clientdb = require('../clientdb'),
|
||||
config = require('../config.js'),
|
||||
constants = require('../constants.js'),
|
||||
DatabaseError = require('../databaseerror'),
|
||||
debug = require('debug')('box:routes/oauth2'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
middleware = require('../middleware/index.js'),
|
||||
oauth2orize = require('oauth2orize'),
|
||||
passport = require('passport'),
|
||||
querystring = require('querystring'),
|
||||
util = require('util'),
|
||||
session = require('connect-ensure-login'),
|
||||
tokendb = require('../tokendb'),
|
||||
appdb = require('../appdb'),
|
||||
url = require('url'),
|
||||
user = require('../user.js'),
|
||||
UserError = user.UserError,
|
||||
hat = require('hat');
|
||||
|
||||
// create OAuth 2.0 server
|
||||
var gServer = oauth2orize.createServer();
|
||||
|
||||
// Register serialialization and deserialization functions.
|
||||
//
|
||||
// When a client redirects a user to user authorization endpoint, an
|
||||
// authorization transaction is initiated. To complete the transaction, the
|
||||
// user must authenticate and approve the authorization request. Because this
|
||||
// may involve multiple HTTP request/response exchanges, the transaction is
|
||||
// stored in the session.
|
||||
//
|
||||
// An application must supply serialization functions, which determine how the
|
||||
// client object is serialized into the session. Typically this will be a
|
||||
// simple matter of serializing the client's ID, and deserializing by finding
|
||||
// the client by ID from the database.
|
||||
|
||||
gServer.serializeClient(function (client, callback) {
|
||||
debug('server serialize:', client);
|
||||
|
||||
return callback(null, client.id);
|
||||
});
|
||||
|
||||
gServer.deserializeClient(function (id, callback) {
|
||||
debug('server deserialize:', id);
|
||||
|
||||
clientdb.get(id, function (error, client) {
|
||||
if (error) { return callback(error); }
|
||||
return callback(null, client);
|
||||
});
|
||||
});
|
||||
|
||||
// Register supported grant types.
|
||||
|
||||
// Grant authorization codes. The callback takes the `client` requesting
|
||||
// authorization, the `redirectURI` (which is used as a verifier in the
|
||||
// subsequent exchange), the authenticated `user` granting access, and
|
||||
// their response, which contains approved scope, duration, etc. as parsed by
|
||||
// the application. The application issues a code, which is bound to these
|
||||
// values, and will be exchanged for an access token.
|
||||
|
||||
// we use , (comma) as scope separator
|
||||
gServer.grant(oauth2orize.grant.code({ scopeSeparator: ',' }, function (client, redirectURI, user, ares, callback) {
|
||||
debug('grant code:', client, redirectURI, user.id, ares);
|
||||
|
||||
var code = hat(256);
|
||||
var expiresAt = Date.now() + 60 * 60000; // 1 hour
|
||||
var scopes = client.scope ? client.scope.split(',') : ['profile','roleUser'];
|
||||
|
||||
if (scopes.indexOf('roleAdmin') !== -1 && !user.admin) {
|
||||
debug('grant code: not allowed, you need to be admin');
|
||||
return callback(new Error('Admin capabilities required'));
|
||||
}
|
||||
|
||||
authcodedb.add(code, client.id, user.username, expiresAt, function (error) {
|
||||
if (error) return callback(error);
|
||||
callback(null, code);
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client, user, ares, callback) {
|
||||
debug('grant token:', client.id, user.id, ares);
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
|
||||
|
||||
tokendb.add(token, tokendb.PREFIX_USER + user.id, client.id, expires, client.scope, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('new access token for client ' + client.id + ' token ' + token);
|
||||
|
||||
callback(null, token);
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
// Exchange authorization codes for access tokens. The callback accepts the
|
||||
// `client`, which is exchanging `code` and any `redirectURI` from the
|
||||
// authorization request for verification. If these values are validated, the
|
||||
// application issues an access token on behalf of the user who authorized the
|
||||
// code.
|
||||
|
||||
gServer.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, callback) {
|
||||
debug('exchange:', client, code, redirectURI);
|
||||
|
||||
authcodedb.get(code, function (error, authCode) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (client.id !== authCode.clientId) return callback(null, false);
|
||||
|
||||
authcodedb.del(code, function (error) {
|
||||
if(error) return callback(error);
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
|
||||
|
||||
tokendb.add(token, tokendb.PREFIX_USER + authCode.userId, authCode.clientId, expires, client.scope, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('new access token for client ' + client.id + ' token ' + token);
|
||||
|
||||
callback(null, token);
|
||||
});
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
// overwrite the session.ensureLoggedIn to not use res.redirect() due to a chrome bug not sending cookies on redirects
|
||||
session.ensureLoggedIn = function (redirectTo) {
|
||||
assert.strictEqual(typeof redirectTo, 'string');
|
||||
|
||||
return function (req, res, next) {
|
||||
if (!req.isAuthenticated || !req.isAuthenticated()) {
|
||||
if (req.session) {
|
||||
req.session.returnTo = req.originalUrl || req.url;
|
||||
}
|
||||
|
||||
res.status(200).send(util.format('<script>window.location.href = "%s";</script>', redirectTo));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function sendErrorPageOrRedirect(req, res, message) {
|
||||
assert.strictEqual(typeof req, 'object');
|
||||
assert.strictEqual(typeof res, 'object');
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
debug('sendErrorPageOrRedirect: returnTo "%s".', req.query.returnTo, message);
|
||||
|
||||
if (typeof req.query.returnTo !== 'string') {
|
||||
res.render('error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: message
|
||||
});
|
||||
} else {
|
||||
var u = url.parse(req.query.returnTo);
|
||||
if (!u.protocol || !u.host) return res.render('error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: 'Invalid request. returnTo query is not a valid URI. ' + message
|
||||
});
|
||||
|
||||
res.redirect(util.format('%s//%s', u.protocol, u.host));
|
||||
}
|
||||
}
|
||||
|
||||
function sendError(req, res, message) {
|
||||
assert.strictEqual(typeof req, 'object');
|
||||
assert.strictEqual(typeof res, 'object');
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
res.render('error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: message
|
||||
});
|
||||
}
|
||||
|
||||
// Main login form username and password
|
||||
function loginForm(req, res) {
|
||||
if (typeof req.session.returnTo !== 'string') return sendErrorPageOrRedirect(req, res, 'Invalid login request. No returnTo provided.');
|
||||
|
||||
var u = url.parse(req.session.returnTo, true);
|
||||
if (!u.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid login request. No client_id provided.');
|
||||
|
||||
function render(applicationName) {
|
||||
res.render('login', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
csrf: req.csrfToken(),
|
||||
applicationName: applicationName,
|
||||
error: req.query.error || null
|
||||
});
|
||||
}
|
||||
|
||||
clientdb.get(u.query.client_id, function (error, result) {
|
||||
if (error) return sendError(req, res, 'Unknown OAuth client');
|
||||
|
||||
// Handle our different types of oauth clients
|
||||
var appId = result.appId;
|
||||
if (appId === constants.ADMIN_CLIENT_ID) {
|
||||
return render(constants.ADMIN_NAME);
|
||||
} else if (appId === constants.TEST_CLIENT_ID) {
|
||||
return render(constants.TEST_NAME);
|
||||
} else if (appId.indexOf('external-') === 0) {
|
||||
return render('External Application');
|
||||
} else if (appId.indexOf('addon-') === 0) {
|
||||
appId = appId.slice('addon-'.length);
|
||||
} else if (appId.indexOf('proxy-') === 0) {
|
||||
appId = appId.slice('proxy-'.length);
|
||||
}
|
||||
|
||||
appdb.get(appId, function (error, result) {
|
||||
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
|
||||
|
||||
var applicationName = result.location || config.fqdn();
|
||||
render(applicationName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// performs the login POST from the login form
|
||||
function login(req, res) {
|
||||
var returnTo = req.session.returnTo || req.query.returnTo;
|
||||
|
||||
var failureQuery = querystring.stringify({ error: 'Invalid username or password', returnTo: returnTo });
|
||||
passport.authenticate('local', {
|
||||
failureRedirect: '/api/v1/session/login?' + failureQuery
|
||||
})(req, res, function () {
|
||||
res.redirect(returnTo);
|
||||
});
|
||||
}
|
||||
|
||||
// ends the current session
|
||||
function logout(req, res) {
|
||||
req.logout();
|
||||
|
||||
if (req.query && req.query.redirect) res.redirect(req.query.redirect);
|
||||
else res.redirect('/');
|
||||
}
|
||||
|
||||
// Form to enter email address to send a password reset request mail
|
||||
// -> GET /api/v1/session/password/resetRequest.html
|
||||
function passwordResetRequestSite(req, res) {
|
||||
res.render('password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken() });
|
||||
}
|
||||
|
||||
// This route is used for above form submission
|
||||
// -> POST /api/v1/session/password/resetRequest
|
||||
function passwordResetRequest(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.identifier !== 'string') return next(new HttpError(400, 'Missing identifier'));
|
||||
|
||||
debug('passwordResetRequest: email or username %s.', req.body.identifier);
|
||||
|
||||
user.resetPasswordByIdentifier(req.body.identifier, function (error) {
|
||||
if (error && error.reason !== UserError.NOT_FOUND) {
|
||||
console.error(error);
|
||||
return sendErrorPageOrRedirect(req, res, 'User not found');
|
||||
}
|
||||
|
||||
res.redirect('/api/v1/session/password/sent.html');
|
||||
});
|
||||
}
|
||||
|
||||
// -> GET /api/v1/session/password/sent.html
|
||||
function passwordSentSite(req, res) {
|
||||
res.render('password_reset_sent', { adminOrigin: config.adminOrigin() });
|
||||
}
|
||||
|
||||
// -> GET /api/v1/session/password/setup.html
|
||||
function passwordSetupSite(req, res, next) {
|
||||
if (!req.query.reset_token) return next(new HttpError(400, 'Missing reset_token'));
|
||||
|
||||
debug('passwordSetupSite: with token %s.', req.query.reset_token);
|
||||
|
||||
user.getByResetToken(req.query.reset_token, function (error, user) {
|
||||
if (error) return next(new HttpError(401, 'Invalid reset_token'));
|
||||
|
||||
res.render('password_setup', { adminOrigin: config.adminOrigin(), user: user, csrf: req.csrfToken(), resetToken: req.query.reset_token });
|
||||
});
|
||||
}
|
||||
|
||||
// -> GET /api/v1/session/password/reset.html
|
||||
function passwordResetSite(req, res, next) {
|
||||
if (!req.query.reset_token) return next(new HttpError(400, 'Missing reset_token'));
|
||||
|
||||
debug('passwordResetSite: with token %s.', req.query.reset_token);
|
||||
|
||||
user.getByResetToken(req.query.reset_token, function (error, user) {
|
||||
if (error) return next(new HttpError(401, 'Invalid reset_token'));
|
||||
|
||||
res.render('password_reset', { adminOrigin: config.adminOrigin(), user: user, csrf: req.csrfToken(), resetToken: req.query.reset_token });
|
||||
});
|
||||
}
|
||||
|
||||
// -> POST /api/v1/session/password/reset
|
||||
function passwordReset(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
|
||||
|
||||
debug('passwordReset: with token %s.', req.body.resetToken);
|
||||
|
||||
user.getByResetToken(req.body.resetToken, function (error, userObject) {
|
||||
if (error) return next(new HttpError(401, 'Invalid resetToken'));
|
||||
|
||||
// setPassword clears the resetToken
|
||||
user.setPassword(userObject.id, req.body.password, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
The callback page takes the redirectURI and the authCode and redirects the browser accordingly
|
||||
|
||||
*/
|
||||
var callback = [
|
||||
session.ensureLoggedIn('/api/v1/session/login'),
|
||||
function (req, res) {
|
||||
debug('callback: with callback server ' + req.query.redirectURI);
|
||||
res.render('callback', { adminOrigin: config.adminOrigin(), callbackServer: req.query.redirectURI });
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
/*
|
||||
|
||||
This indicates a missing OAuth client session or invalid client ID
|
||||
|
||||
*/
|
||||
var error = [
|
||||
session.ensureLoggedIn('/api/v1/session/login'),
|
||||
function (req, res) {
|
||||
sendErrorPageOrRedirect(req, res, 'Invalid OAuth Client');
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
/*
|
||||
|
||||
The authorization endpoint is the entry point for an OAuth login.
|
||||
|
||||
Each app would start OAuth by redirecting the user to:
|
||||
|
||||
/api/v1/oauth/dialog/authorize?response_type=code&client_id=<clientId>&redirect_uri=<callbackURL>&scope=<ignored>
|
||||
|
||||
- First, this will ensure the user is logged in.
|
||||
- Then in normal OAuth it would ask the user for permissions to the scopes, which we will do on app installation
|
||||
- Then it will redirect the browser to the given <callbackURL> containing the authcode in the query
|
||||
|
||||
Scopes are set by the app during installation, the ones given on OAuth transaction start are simply ignored.
|
||||
|
||||
*/
|
||||
var authorization = [
|
||||
// extract the returnTo origin and set as query param
|
||||
function (req, res, next) {
|
||||
if (!req.query.redirect_uri) return sendErrorPageOrRedirect(req, res, 'Invalid request. redirect_uri query param is not set.');
|
||||
if (!req.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid request. client_id query param is not set.');
|
||||
if (!req.query.response_type) return sendErrorPageOrRedirect(req, res, 'Invalid request. response_type query param is not set.');
|
||||
if (req.query.response_type !== 'code' && req.query.response_type !== 'token') return sendErrorPageOrRedirect(req, res, 'Invalid request. Only token and code response types are supported.');
|
||||
|
||||
session.ensureLoggedIn('/api/v1/session/login?returnTo=' + req.query.redirect_uri)(req, res, next);
|
||||
},
|
||||
gServer.authorization(function (clientID, redirectURI, callback) {
|
||||
debug('authorization: client %s with callback to %s.', clientID, redirectURI);
|
||||
|
||||
clientdb.get(clientID, function (error, client) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
// ignore the origin passed into form the client, but use the one from the clientdb
|
||||
var redirectPath = url.parse(redirectURI).path;
|
||||
var redirectOrigin = client.redirectURI;
|
||||
|
||||
callback(null, client, '/api/v1/session/callback?redirectURI=' + url.resolve(redirectOrigin, redirectPath));
|
||||
});
|
||||
}),
|
||||
// Until we have OAuth scopes, skip decision dialog
|
||||
// OAuth sopes skip START
|
||||
function (req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.oauth2, 'object');
|
||||
|
||||
var scopes = req.oauth2.client.scope ? req.oauth2.client.scope.split(',') : ['profile','roleUser'];
|
||||
|
||||
if (scopes.indexOf('roleAdmin') !== -1 && !req.user.admin) {
|
||||
return sendErrorPageOrRedirect(req, res, 'Admin capabilities required');
|
||||
}
|
||||
|
||||
req.body.transaction_id = req.oauth2.transactionID;
|
||||
next();
|
||||
},
|
||||
gServer.decision(function(req, done) {
|
||||
debug('decision: with scope', req.oauth2.req.scope);
|
||||
return done(null, { scope: req.oauth2.req.scope });
|
||||
})
|
||||
// OAuth sopes skip END
|
||||
// function (req, res) {
|
||||
// res.render('dialog', { transactionID: req.oauth2.transactionID, user: req.user, client: req.oauth2.client, csrf: req.csrfToken() });
|
||||
// }
|
||||
];
|
||||
|
||||
// this triggers the above grant middleware and handles the user's decision if he accepts the access
|
||||
var decision = [
|
||||
session.ensureLoggedIn('/api/v1/session/login'),
|
||||
gServer.decision()
|
||||
];
|
||||
|
||||
|
||||
/*
|
||||
|
||||
The token endpoint allows an OAuth client to exchange an authcode with an accesstoken.
|
||||
|
||||
Authcodes are obtained using the authorization endpoint. The route is authenticated by
|
||||
providing a Basic auth with clientID as username and clientSecret as password.
|
||||
An authcode is only good for one such exchange to an accesstoken.
|
||||
|
||||
*/
|
||||
var token = [
|
||||
passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
|
||||
gServer.token(),
|
||||
gServer.errorHandler()
|
||||
];
|
||||
|
||||
|
||||
/*
|
||||
|
||||
The scope middleware provides an auth middleware for routes.
|
||||
|
||||
It is used for API routes, which are authenticated using accesstokens.
|
||||
Those accesstokens carry OAuth scopes and the middleware takes the required
|
||||
scope as an argument and will verify the accesstoken against it.
|
||||
|
||||
See server.js:
|
||||
var profileScope = routes.oauth2.scope('profile');
|
||||
|
||||
*/
|
||||
function scope(requestedScope) {
|
||||
assert.strictEqual(typeof requestedScope, 'string');
|
||||
|
||||
var requestedScopes = requestedScope.split(',');
|
||||
debug('scope: add routes with requested scopes', requestedScopes);
|
||||
|
||||
return [
|
||||
passport.authenticate(['bearer'], { session: false }),
|
||||
function (req, res, next) {
|
||||
if (!req.authInfo || !req.authInfo.scope) return next(new HttpError(401, 'No scope found'));
|
||||
if (req.authInfo.scope === '*') return next();
|
||||
|
||||
var scopes = req.authInfo.scope.split(',');
|
||||
|
||||
for (var i = 0; i < requestedScopes.length; ++i) {
|
||||
if (scopes.indexOf(requestedScopes[i]) === -1) {
|
||||
debug('scope: missing scope "%s".', requestedScopes[i]);
|
||||
return next(new HttpError(401, 'Missing required scope "' + requestedScopes[i] + '"'));
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Cross-site request forgery protection middleware for login form
|
||||
var csrf = [
|
||||
middleware.csrf(),
|
||||
function (err, req, res, next) {
|
||||
if (err.code !== 'EBADCSRFTOKEN') return next(err);
|
||||
|
||||
sendErrorPageOrRedirect(req, res, 'Form expired');
|
||||
}
|
||||
];
|
||||
|
||||
exports = module.exports = {
|
||||
loginForm: loginForm,
|
||||
login: login,
|
||||
logout: logout,
|
||||
callback: callback,
|
||||
error: error,
|
||||
passwordResetRequestSite: passwordResetRequestSite,
|
||||
passwordResetRequest: passwordResetRequest,
|
||||
passwordSentSite: passwordSentSite,
|
||||
passwordResetSite: passwordResetSite,
|
||||
passwordSetupSite: passwordSetupSite,
|
||||
passwordReset: passwordReset,
|
||||
authorization: authorization,
|
||||
decision: decision,
|
||||
token: token,
|
||||
scope: scope,
|
||||
csrf: csrf
|
||||
};
|
||||
82
src/routes/settings.js
Normal file
82
src/routes/settings.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getAutoupdatePattern: getAutoupdatePattern,
|
||||
setAutoupdatePattern: setAutoupdatePattern,
|
||||
|
||||
getCloudronName: getCloudronName,
|
||||
setCloudronName: setCloudronName,
|
||||
|
||||
getCloudronAvatar: getCloudronAvatar,
|
||||
setCloudronAvatar: setCloudronAvatar
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
safe = require('safetydance'),
|
||||
settings = require('../settings.js'),
|
||||
SettingsError = settings.SettingsError;
|
||||
|
||||
function getAutoupdatePattern(req, res, next) {
|
||||
settings.getAutoupdatePattern(function (error, pattern) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { pattern: pattern }));
|
||||
});
|
||||
}
|
||||
|
||||
function setAutoupdatePattern(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
|
||||
|
||||
settings.setAutoupdatePattern(req.body.pattern, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, 'Invalid pattern'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
});
|
||||
}
|
||||
|
||||
function setCloudronName(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name is required'));
|
||||
|
||||
settings.setCloudronName(req.body.name, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, 'Invalid name'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200));
|
||||
});
|
||||
}
|
||||
|
||||
function getCloudronName(req, res, next) {
|
||||
settings.getCloudronName(function (error, name) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, { name: name }));
|
||||
});
|
||||
}
|
||||
|
||||
function setCloudronAvatar(req, res, next) {
|
||||
assert.strictEqual(typeof req.files, 'object');
|
||||
|
||||
if (!req.files.avatar) return next(new HttpError(400, 'avatar must be provided'));
|
||||
var avatar = safe.fs.readFileSync(req.files.avatar.path);
|
||||
|
||||
settings.setCloudronAvatar(avatar, function (error) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getCloudronAvatar(req, res, next) {
|
||||
settings.getCloudronAvatar(function (error, avatar) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.set('Content-Type', 'image/png');
|
||||
res.status(200).send(avatar);
|
||||
});
|
||||
}
|
||||
1319
src/routes/test/apps-test.js
Normal file
1319
src/routes/test/apps-test.js
Normal file
File diff suppressed because it is too large
Load Diff
144
src/routes/test/backups-test.js
Normal file
144
src/routes/test/backups-test.js
Normal file
@@ -0,0 +1,144 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var appdb = require('../../appdb.js'),
|
||||
async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
request = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
nock = require('nock'),
|
||||
userdb = require('../../userdb.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var token = null;
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
|
||||
userdb._clear,
|
||||
|
||||
function createAdmin(callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function addApp(callback) {
|
||||
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok' };
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Backups API', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
describe('get', function () {
|
||||
it('cannot get backups with appstore request failing', function (done) {
|
||||
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=APPSTORE_TOKEN').reply(401, {});
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/backups')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(503);
|
||||
expect(req.isDone()).to.be.ok();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('can get backups', function (done) {
|
||||
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=APPSTORE_TOKEN').reply(200, { backups: ['foo', 'bar']});
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/backups')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(req.isDone()).to.be.ok();
|
||||
expect(res.body.backups).to.be.an(Array);
|
||||
expect(res.body.backups[0]).to.eql('foo');
|
||||
expect(res.body.backups[1]).to.eql('bar');
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', function () {
|
||||
it('fails due to mising token', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/backups')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong token', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/backups')
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var scope = nock(config.apiServerOrigin())
|
||||
.put('/api/v1/boxes/' + config.fqdn() + '/backupurl?token=APPSTORE_TOKEN', { boxVersion: '0.5.0', appId: null, appVersion: null, appBackupIds: [] })
|
||||
.reply(201, { id: 'someid', url: 'http://foobar', backupKey: 'somerestorekey' });
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/backups')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope.isDone()) {
|
||||
return done();
|
||||
}
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
}
|
||||
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
817
src/routes/test/clients-test.js
Normal file
817
src/routes/test/clients-test.js
Normal file
@@ -0,0 +1,817 @@
|
||||
'use strict';
|
||||
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
var async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
oauth2 = require('../oauth2.js'),
|
||||
expect = require('expect.js'),
|
||||
uuid = require('node-uuid'),
|
||||
nock = require('nock'),
|
||||
hat = require('hat'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var token = null; // authentication token
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('OAuth Clients API', function () {
|
||||
describe('add', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
server.start.bind(null),
|
||||
database._clear.bind(null),
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if not in developerMode', function (done) {
|
||||
config.set('developerMode', false);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(412);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without appId', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.send({ redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty appId', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: '', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without scope', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty scope', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without redirectURI', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', scope: 'profile,roleUser' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty redirectURI', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', redirectURI: '', scope: 'profile,roleUser' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with malformed redirectURI', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', redirectURI: 'foobar', scope: 'profile,roleUser' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(result.body.id).to.be.a('string');
|
||||
expect(result.body.appId).to.be.a('string');
|
||||
expect(result.body.redirectURI).to.be.a('string');
|
||||
expect(result.body.clientSecret).to.be.a('string');
|
||||
expect(result.body.scope).to.be.a('string');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', function () {
|
||||
var CLIENT_0 = {
|
||||
id: '',
|
||||
appId: 'someAppId-0',
|
||||
redirectURI: 'http://some.callback0',
|
||||
scope: 'profile,roleUser'
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
async.series([
|
||||
server.start.bind(null),
|
||||
database._clear.bind(null),
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function (callback) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
|
||||
CLIENT_0 = result.body;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if not in developerMode', function (done) {
|
||||
config.set('developerMode', false);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(412);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with unknown id', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body).to.eql(CLIENT_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', function () {
|
||||
var CLIENT_0 = {
|
||||
id: '',
|
||||
appId: 'someAppId-0',
|
||||
redirectURI: 'http://some.callback0',
|
||||
scope: 'profile,roleUser'
|
||||
};
|
||||
var CLIENT_1 = {
|
||||
id: '',
|
||||
appId: 'someAppId-1',
|
||||
redirectURI: 'http://some.callback1',
|
||||
scope: 'profile,roleUser'
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
async.series([
|
||||
server.start.bind(null),
|
||||
database._clear.bind(null),
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function (callback) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
|
||||
CLIENT_0 = result.body;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if not in developerMode', function (done) {
|
||||
config.set('developerMode', false);
|
||||
|
||||
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.send({ appId: CLIENT_1.appId, redirectURI: CLIENT_1.redirectURI })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(412);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without appId', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.send({ redirectURI: CLIENT_1.redirectURI })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty appId', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.send({ appId: '', redirectURI: CLIENT_1.redirectURI })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without redirectURI', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.send({ appId: CLIENT_1.appId })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty redirectURI', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.send({ appId: CLIENT_1.appId, redirectURI: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with malformed redirectURI', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.send({ appId: CLIENT_1.appId, redirectURI: 'foobar' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.send({ appId: CLIENT_1.appId, redirectURI: CLIENT_1.redirectURI })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.appId).to.equal(CLIENT_1.appId);
|
||||
expect(result.body.redirectURI).to.equal(CLIENT_1.redirectURI);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('del', function () {
|
||||
var CLIENT_0 = {
|
||||
id: '',
|
||||
appId: 'someAppId-0',
|
||||
redirectURI: 'http://some.callback0',
|
||||
scope: 'profile,roleUser'
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
async.series([
|
||||
server.start.bind(null),
|
||||
database._clear.bind(null),
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function (callback) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
|
||||
CLIENT_0 = result.body;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if not in developerMode', function (done) {
|
||||
config.set('developerMode', false);
|
||||
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(412);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with unknown id', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(404);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Clients', function () {
|
||||
var USER_0 = {
|
||||
userId: uuid.v4(),
|
||||
username: 'someusername',
|
||||
password: 'somepassword',
|
||||
email: 'some@email.com',
|
||||
admin: true,
|
||||
salt: 'somesalt',
|
||||
createdAt: (new Date()).toUTCString(),
|
||||
modifiedAt: (new Date()).toUTCString(),
|
||||
resetToken: hat(256)
|
||||
};
|
||||
|
||||
// make csrf always succeed for testing
|
||||
oauth2.csrf = function (req, res, next) {
|
||||
req.csrfToken = function () { return hat(256); };
|
||||
next();
|
||||
};
|
||||
|
||||
function setup(done) {
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
database._clear.bind(null),
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USER_0.username, password: USER_0.password, email: USER_0.email })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('get', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('fails due to missing token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
expect(result.body.clients.length).to.eql(1);
|
||||
expect(result.body.clients[0].tokenCount).to.eql(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get tokens by client', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('fails due to missing token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to unkown client', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/CID-WEBADMIN/tokens')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
expect(result.body.tokens.length).to.eql(1);
|
||||
expect(result.body.tokens[0].identifier).to.eql('user-' + USER_0.username);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete tokens by client', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('fails due to missing token', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty token', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong token', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to unkown client', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/CID-WEBADMIN/tokens')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
expect(result.body.tokens.length).to.eql(1);
|
||||
expect(result.body.tokens[0].identifier).to.eql('user-' + USER_0.username);
|
||||
|
||||
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
// further calls with this token should not work
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
506
src/routes/test/cloudron-test.js
Normal file
506
src/routes/test/cloudron-test.js
Normal file
@@ -0,0 +1,506 @@
|
||||
'use strict';
|
||||
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
var async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
nock = require('nock'),
|
||||
paths = require('../../paths.js'),
|
||||
request = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
shell = require('../../shell.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var token = null; // authentication token
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
config.set('version', '0.5.0');
|
||||
server.start(done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
var gSudoOriginal = null;
|
||||
function injectShellMock() {
|
||||
gSudoOriginal = shell.sudo;
|
||||
shell.sudo = function (tag, options, callback) { callback(null); };
|
||||
}
|
||||
|
||||
function restoreShellMock() {
|
||||
shell.sudo = gSudoOriginal;
|
||||
}
|
||||
|
||||
describe('Cloudron', function () {
|
||||
|
||||
describe('activate', function () {
|
||||
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('fails due to missing setupToken', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty username', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty password', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: '', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty email', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty name', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: '', email: 'admin@foo.bar', name: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to invalid email', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'invalidemail' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar', name: 'tester' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails the second time', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(409);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Certificates API', function () {
|
||||
var certFile, keyFile;
|
||||
|
||||
before(function (done) {
|
||||
certFile = path.join(os.tmpdir(), 'host.cert');
|
||||
fs.writeFileSync(certFile, 'test certificate');
|
||||
|
||||
keyFile = path.join(os.tmpdir(), 'host.key');
|
||||
fs.writeFileSync(keyFile, 'test key');
|
||||
|
||||
async.series([
|
||||
setup,
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
], done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
fs.unlinkSync(certFile);
|
||||
fs.unlinkSync(keyFile);
|
||||
|
||||
cleanup(done);
|
||||
});
|
||||
|
||||
it('cannot set certificate without token', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/certificate')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set certificate without certificate', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/certificate')
|
||||
.query({ access_token: token })
|
||||
.attach('key', keyFile, 'key')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set certificate without key', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/certificate')
|
||||
.query({ access_token: token })
|
||||
.attach('certificate', certFile, 'certificate')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set certificate', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/certificate')
|
||||
.query({ access_token: token })
|
||||
.attach('key', keyFile, 'key')
|
||||
.attach('certificate', certFile, 'certificate')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('did set the certificate', function (done) {
|
||||
var cert = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'));
|
||||
expect(cert).to.eql(fs.readFileSync(certFile));
|
||||
|
||||
var key = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'));
|
||||
expect(key).to.eql(fs.readFileSync(keyFile));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get config', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
setup,
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
config._reset();
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('cannot get without token', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds without appstore', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
|
||||
expect(result.body.webServerOrigin).to.eql(null);
|
||||
expect(result.body.fqdn).to.eql('localhost');
|
||||
expect(result.body.isCustomDomain).to.eql(false);
|
||||
expect(result.body.progress).to.be.an('object');
|
||||
expect(result.body.update).to.be.an('object');
|
||||
expect(result.body.version).to.eql('0.5.0');
|
||||
expect(result.body.developerMode).to.be.a('boolean');
|
||||
expect(result.body.size).to.eql(null);
|
||||
expect(result.body.region).to.eql(null);
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/localhost?token=' + config.token()).reply(200, { box: { region: 'sfo', size: 'small' }});
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
|
||||
expect(result.body.webServerOrigin).to.eql(null);
|
||||
expect(result.body.fqdn).to.eql('localhost');
|
||||
expect(result.body.isCustomDomain).to.eql(false);
|
||||
expect(result.body.progress).to.be.an('object');
|
||||
expect(result.body.update).to.be.an('object');
|
||||
expect(result.body.version).to.eql('0.5.0');
|
||||
expect(result.body.developerMode).to.be.a('boolean');
|
||||
expect(result.body.size).to.eql('small');
|
||||
expect(result.body.region).to.eql('sfo');
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('migrate', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
setup,
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
config._reset();
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo'})
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo'})
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with missing size', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with wrong size type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 4, region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with missing region', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('fails with wrong region type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 4, password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when in wrong state', function (done) {
|
||||
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', { size: 'small', region: 'sfo', restoreKey: 'someid' }).reply(409, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).put('/api/v1/boxes/' + config.fqdn() + '/backupurl?token=APPSTORE_TOKEN', { boxVersion: '0.5.0', appId: null, appVersion: null, appBackupIds: [] }).reply(201, { id: 'someid', url: 'http://foobar', backupKey: 'somerestorekey' });
|
||||
|
||||
injectShellMock();
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope1.isDone() && scope2.isDone()) {
|
||||
restoreShellMock();
|
||||
return done();
|
||||
}
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
}
|
||||
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', { size: 'small', region: 'sfo', restoreKey: 'someid' }).reply(202, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).put('/api/v1/boxes/' + config.fqdn() + '/backupurl?token=APPSTORE_TOKEN', { boxVersion: '0.5.0', appId: null, appVersion: null, appBackupIds: [] }).reply(201, { id: 'someid', url: 'http://foobar', backupKey: 'somerestorekey' });
|
||||
|
||||
injectShellMock();
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope1.isDone() && scope2.isDone()) {
|
||||
restoreShellMock();
|
||||
return done();
|
||||
}
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
}
|
||||
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
356
src/routes/test/developer-test.js
Normal file
356
src/routes/test/developer-test.js
Normal file
@@ -0,0 +1,356 @@
|
||||
'use strict';
|
||||
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
var async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
nock = require('nock'),
|
||||
request = require('superagent'),
|
||||
server = require('../../server.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var token = null; // authentication token
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
server.start(done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Developer API', function () {
|
||||
describe('isEnabled', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
setup,
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/developer')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds (enabled)', function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds (not enabled)', function (done) {
|
||||
config.set('developerMode', false);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(412);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEnabled', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
setup,
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
.send({ enabled: true })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ enabled: true })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ password: '', enabled: true })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD.toUpperCase(), enabled: true })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing enabled property', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong enabled property type', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, enabled: 'true' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds enabling', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, enabled: true })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds disabling', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, enabled: false })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/developer')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(412);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', function () {
|
||||
before(function (done) {
|
||||
config.set('developerMode', true);
|
||||
|
||||
async.series([
|
||||
setup,
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('fails without body', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without username', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: USERNAME })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty username', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: '', password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: USERNAME, password: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with unknown username', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: USERNAME.toUpperCase(), password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with wrong password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: USERNAME, password: PASSWORD.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('with username succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: USERNAME, password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.expiresAt).to.be.a('number');
|
||||
expect(result.body.token).to.be.a('string');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('with email succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: EMAIL, password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.expiresAt).to.be.a('number');
|
||||
expect(result.body.token).to.be.a('string');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
334
src/routes/test/oauth2-test.js
Normal file
334
src/routes/test/oauth2-test.js
Normal file
@@ -0,0 +1,334 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var expect = require('expect.js'),
|
||||
uuid = require('node-uuid'),
|
||||
hat = require('hat'),
|
||||
nock = require('nock'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
oauth2 = require('../oauth2.js'),
|
||||
server = require('../../server.js'),
|
||||
database = require('../../database.js'),
|
||||
userdb = require('../../userdb.js'),
|
||||
config = require('../../config.js'),
|
||||
superagent = require('superagent'),
|
||||
passport = require('passport');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
describe('OAuth2', function () {
|
||||
var passportAuthenticateSave = null;
|
||||
|
||||
before(function () {
|
||||
passportAuthenticateSave = passport.authenticate;
|
||||
passport.authenticate = function () {
|
||||
return function (req, res, next) { next(); };
|
||||
};
|
||||
});
|
||||
|
||||
after(function () {
|
||||
passport.authenticate = passportAuthenticateSave;
|
||||
});
|
||||
|
||||
describe('scopes middleware', function () {
|
||||
it('fails due to missing authInfo', function (done) {
|
||||
var mw = oauth2.scope('admin')[1];
|
||||
var req = {};
|
||||
|
||||
mw(req, null, function (error) {
|
||||
expect(error).to.be.a(HttpError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing scope property in authInfo', function (done) {
|
||||
var mw = oauth2.scope('admin')[1];
|
||||
var req = { authInfo: {} };
|
||||
|
||||
mw(req, null, function (error) {
|
||||
expect(error).to.be.a(HttpError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing scope in request', function (done) {
|
||||
var mw = oauth2.scope('admin')[1];
|
||||
var req = { authInfo: { scope: '' } };
|
||||
|
||||
mw(req, null, function (error) {
|
||||
expect(error).to.be.a(HttpError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong scope in request', function (done) {
|
||||
var mw = oauth2.scope('admin')[1];
|
||||
var req = { authInfo: { scope: 'foobar,something' } };
|
||||
|
||||
mw(req, null, function (error) {
|
||||
expect(error).to.be.a(HttpError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong scope in request', function (done) {
|
||||
var mw = oauth2.scope('admin,users')[1];
|
||||
var req = { authInfo: { scope: 'foobar,admin' } };
|
||||
|
||||
mw(req, null, function (error) {
|
||||
expect(error).to.be.a(HttpError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with one requested scope and one provided scope', function (done) {
|
||||
var mw = oauth2.scope('admin')[1];
|
||||
var req = { authInfo: { scope: 'admin' } };
|
||||
|
||||
mw(req, null, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with one requested scope and two provided scopes', function (done) {
|
||||
var mw = oauth2.scope('admin')[1];
|
||||
var req = { authInfo: { scope: 'foobar,admin' } };
|
||||
|
||||
mw(req, null, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with two requested scope and two provided scopes', function (done) {
|
||||
var mw = oauth2.scope('admin,foobar')[1];
|
||||
var req = { authInfo: { scope: 'foobar,admin' } };
|
||||
|
||||
mw(req, null, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with two requested scope and provided wildcard scope', function (done) {
|
||||
var mw = oauth2.scope('admin,foobar')[1];
|
||||
var req = { authInfo: { scope: '*' } };
|
||||
|
||||
mw(req, null, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Password', function () {
|
||||
var USER_0 = {
|
||||
userId: uuid.v4(),
|
||||
username: 'someusername',
|
||||
password: 'somepassword',
|
||||
email: 'some@email.com',
|
||||
admin: true,
|
||||
salt: 'somesalt',
|
||||
createdAt: (new Date()).toUTCString(),
|
||||
modifiedAt: (new Date()).toUTCString(),
|
||||
resetToken: hat(256)
|
||||
};
|
||||
|
||||
// make csrf always succeed for testing
|
||||
oauth2.csrf = function (req, res, next) {
|
||||
req.csrfToken = function () { return hat(256); };
|
||||
next();
|
||||
};
|
||||
|
||||
function setup(done) {
|
||||
server.start(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
database._clear(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
userdb.add(USER_0.userId, USER_0, done);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('pages', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('reset request succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/resetRequest.html')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('setup fails due to missing reset_token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/setup.html')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('setup fails due to invalid reset_token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/setup.html')
|
||||
.query({ reset_token: hat(256) })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('setup succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/setup.html')
|
||||
.query({ reset_token: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('reset fails due to missing reset_token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('reset fails due to invalid reset_token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
|
||||
.query({ reset_token: hat(256) })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('reset succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
|
||||
.query({ reset_token: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('sent succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/sent.html')
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset request handler', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/resetRequest')
|
||||
.send({ identifier: USER_0.email })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset handler', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('fails due to missing resetToken', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: 'somepassword' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ resetToken: hat(256) })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: '', resetToken: hat(256) })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty resetToken', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: '', resetToken: '' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var scope = nock(config.adminOrigin())
|
||||
.filteringPath(function (path) {
|
||||
path = path.replace(/accessToken=[^&]*/, 'accessToken=token');
|
||||
path = path.replace(/expiresAt=[^&]*/, 'expiresAt=1234');
|
||||
return path;
|
||||
})
|
||||
.get('/?accessToken=token&expiresAt=1234').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: 'somepassword', resetToken: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
233
src/routes/test/settings-test.js
Normal file
233
src/routes/test/settings-test.js
Normal file
@@ -0,0 +1,233 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var appdb = require('../../appdb.js'),
|
||||
async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
paths = require('../../paths.js'),
|
||||
request = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js'),
|
||||
fs = require('fs'),
|
||||
nock = require('nock'),
|
||||
userdb = require('../../userdb.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var token = null;
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
|
||||
userdb._clear,
|
||||
|
||||
function createAdmin(callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function addApp(callback) {
|
||||
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok' };
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Settings API', function () {
|
||||
this.timeout(10000);
|
||||
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
describe('autoupdate_pattern', function () {
|
||||
it('can get auto update pattern (default)', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.pattern).to.be.ok();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set autoupdate_pattern without pattern', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set autoupdate_pattern', function (done) {
|
||||
var eventPattern = null;
|
||||
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, function (pattern) {
|
||||
eventPattern = pattern;
|
||||
});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
.query({ access_token: token })
|
||||
.send({ pattern: '00 30 11 * * 1-5' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(eventPattern === '00 30 11 * * 1-5').to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set autoupdate_pattern to never', function (done) {
|
||||
var eventPattern = null;
|
||||
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, function (pattern) {
|
||||
eventPattern = pattern;
|
||||
});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
.query({ access_token: token })
|
||||
.send({ pattern: 'never' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(eventPattern).to.eql('never');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set invalid autoupdate_pattern', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
|
||||
.query({ access_token: token })
|
||||
.send({ pattern: '1 3 x 5 6' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloudron_name', function () {
|
||||
var name = 'foobar';
|
||||
|
||||
it('get default succeeds', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.name).to.be.ok();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set without name', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set empty name', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.send({ name: '' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.send({ name: name })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/settings/cloudron_name')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.name).to.eql(name);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloudron_avatar', function () {
|
||||
it('get default succeeds', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body).to.be.a(Buffer);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set without data', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.attach('avatar', paths.FAVICON_FILE)
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/settings/cloudron_avatar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.toString()).to.eql(fs.readFileSync(paths.FAVICON_FILE, 'utf-8'));
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
68
src/routes/test/start_addons.sh
Executable file
68
src/routes/test/start_addons.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly mysqldatadir="/tmp/mysqldata-$(date +%s)"
|
||||
readonly postgresqldatadir="/tmp/postgresqldata-$(date +%s)"
|
||||
readonly mongodbdatadir="/tmp/mongodbdata-$(date +%s)"
|
||||
root_password=secret
|
||||
|
||||
start_postgresql() {
|
||||
postgresql_vars="POSTGRESQL_ROOT_PASSWORD=${root_password}; POSTGRESQL_ROOT_HOST=172.17.0.0/255.255.0.0"
|
||||
|
||||
if which boot2docker >/dev/null; then
|
||||
boot2docker ssh "sudo rm -rf /tmp/postgresql_vars.sh"
|
||||
boot2docker ssh "echo \"${postgresql_vars}\" > /tmp/postgresql_vars.sh"
|
||||
else
|
||||
rm -rf /tmp/postgresql_vars.sh
|
||||
echo "${postgresql_vars}" > /tmp/postgresql_vars.sh
|
||||
fi
|
||||
|
||||
docker rm -f postgresql 2>/dev/null 1>&2 || true
|
||||
|
||||
docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" -v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh cloudron/postgresql:0.3.0 >/dev/null
|
||||
}
|
||||
|
||||
start_mysql() {
|
||||
local mysql_vars="MYSQL_ROOT_PASSWORD=${root_password}; MYSQL_ROOT_HOST=172.17.0.0/255.255.0.0"
|
||||
|
||||
if which boot2docker >/dev/null; then
|
||||
boot2docker ssh "sudo rm -rf /tmp/mysql_vars.sh"
|
||||
boot2docker ssh "echo \"${mysql_vars}\" > /tmp/mysql_vars.sh"
|
||||
else
|
||||
rm -rf /tmp/mysql_vars.sh
|
||||
echo "${mysql_vars}" > /tmp/mysql_vars.sh
|
||||
fi
|
||||
|
||||
docker rm -f mysql 2>/dev/null 1>&2 || true
|
||||
|
||||
docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" -v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh cloudron/mysql:0.3.0 >/dev/null
|
||||
}
|
||||
|
||||
start_mongodb() {
|
||||
local mongodb_vars="MONGODB_ROOT_PASSWORD=${root_password}"
|
||||
|
||||
if which boot2docker >/dev/null; then
|
||||
boot2docker ssh "sudo rm -rf /tmp/mongodb_vars.sh"
|
||||
boot2docker ssh "echo \"${mongodb_vars}\" > /tmp/mongodb_vars.sh"
|
||||
else
|
||||
rm -rf /tmp/mongodb_vars.sh
|
||||
echo "${mongodb_vars}" > /tmp/mongodb_vars.sh
|
||||
fi
|
||||
|
||||
docker rm -f mongodb 2>/dev/null 1>&2 || true
|
||||
|
||||
docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" -v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh cloudron/mongodb:0.3.0 >/dev/null
|
||||
}
|
||||
|
||||
start_mysql
|
||||
start_postgresql
|
||||
start_mongodb
|
||||
|
||||
echo -n "Waiting for addons to start"
|
||||
for i in {1..10}; do
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
echo ""
|
||||
|
||||
528
src/routes/test/user-test.js
Normal file
528
src/routes/test/user-test.js
Normal file
@@ -0,0 +1,528 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
tokendb = require('../../tokendb.js'),
|
||||
expect = require('expect.js'),
|
||||
request = require('superagent'),
|
||||
nock = require('nock'),
|
||||
server = require('../../server.js'),
|
||||
userdb = require('../../userdb.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME_0 = 'admin', PASSWORD = 'password', EMAIL = 'silly@me.com', EMAIL_0_NEW = 'stupid@me.com';
|
||||
var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'tao@zen.mac';
|
||||
var USERNAME_2 = 'userTheSecond', EMAIL_2 = 'user@foo.bar';
|
||||
var USERNAME_3 = 'userTheThird', EMAIL_3 = 'user3@foo.bar';
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
server.start(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
userdb._clear(done);
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('User API', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
var user_0 = null;
|
||||
var token = null;
|
||||
var token_1 = tokendb.generateToken();
|
||||
var token_2 = tokendb.generateToken();
|
||||
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('device is in first time mode', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/status')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.activated).to.not.be.ok();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('create admin fails due to missing parameters', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME_0 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('create admin fails because only POST is allowed', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('create admin', function (done) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME_0, password: PASSWORD, email: EMAIL })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
// stash for later use
|
||||
token = res.body.token;
|
||||
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('device left first time mode', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/status')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.activated).to.be.ok();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('can get userInfo with token', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0);
|
||||
expect(res.body.email).to.equal(EMAIL);
|
||||
expect(res.body.admin).to.be.ok();
|
||||
|
||||
// stash for further use
|
||||
user_0 = res.body;
|
||||
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get userInfo with expired token', function (done) {
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() + 2000; // 1 sec
|
||||
|
||||
tokendb.add(token, tokendb.PREFIX_USER + user_0.id, null, expires, '*', function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
setTimeout(function () {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + user_0.username)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
it('can get userInfo with token', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0);
|
||||
expect(res.body.email).to.equal(EMAIL);
|
||||
expect(res.body.admin).to.be.ok();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get userInfo only with basic auth', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.auth(USERNAME_0, PASSWORD)
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get userInfo with invalid token (token length)', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: 'x' + token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get userInfo with invalid token (wrong token)', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('can get userInfo with token in auth header', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.set('Authorization', 'Bearer ' + token)
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0);
|
||||
expect(res.body.email).to.equal(EMAIL);
|
||||
expect(res.body.admin).to.be.ok();
|
||||
expect(res.body.password).to.not.be.ok();
|
||||
expect(res.body.salt).to.not.be.ok();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get userInfo with invalid token in auth header', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.set('Authorization', 'Bearer ' + 'x' + token)
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get userInfo with invalid token (wrong token)', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.set('Authorization', 'Bearer ' + 'x' + token.toUpperCase())
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('create second user succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1 })
|
||||
.end(function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 10000, '*', done);
|
||||
});
|
||||
});
|
||||
|
||||
it('set second user as admin succeeds', function (done) {
|
||||
// TODO is USERNAME_1 in body and url redundant?
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, admin: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('remove first user from admins succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ username: USERNAME_0, admin: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('remove second user by first, now normal, user fails', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('remove second user from admins and thus last admin fails', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ username: USERNAME_1, admin: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('reset first user as admin succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ username: USERNAME_0, admin: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('create user missing username fails', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ email: EMAIL_2 })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create user missing email fails', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2 })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create second and third user', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2, email: EMAIL_2 })
|
||||
.end(function (error, res) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_3, email: EMAIL_3 })
|
||||
.end(function (error, res) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_2, tokendb.PREFIX_USER + USERNAME_2, 'test-client-id', Date.now() + 10000, '*', done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('second user userInfo', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.username).to.equal(USERNAME_2);
|
||||
expect(result.body.email).to.equal(EMAIL_2);
|
||||
expect(result.body.admin).to.not.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create user with same username should fail', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2, email: EMAIL })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(409);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('list users', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token_2 })
|
||||
.end(function (error, res) {
|
||||
expect(error).to.be(null);
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.users).to.be.an('array');
|
||||
expect(res.body.users.length).to.equal(4);
|
||||
|
||||
res.body.users.forEach(function (user) {
|
||||
expect(user).to.be.an('object');
|
||||
expect(user.id).to.be.ok();
|
||||
expect(user.username).to.be.ok();
|
||||
expect(user.email).to.be.ok();
|
||||
expect(user.password).to.not.be.ok();
|
||||
expect(user.salt).to.not.be.ok();
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('user removes himself is not allowed', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('admin cannot remove normal user without giving a password', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('admin cannot remove normal user with empty password', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
.query({ access_token: token })
|
||||
.send({ password: '' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('admin cannot remove normal user with giving wrong password', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD + PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('admin removes normal user', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('admin removes himself should not be allowed', function (done) {
|
||||
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
// Change email
|
||||
it('change email fails due to missing token', function (done) {
|
||||
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.send({ password: PASSWORD, email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
it('change email fails due to missing password', function (done) {
|
||||
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
it('change email fails due to wrong password', function (done) {
|
||||
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD+PASSWORD, email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(403);
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
it('change email fails due to invalid email', function (done) {
|
||||
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, email: 'foo@bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
it('change email succeeds', function (done) {
|
||||
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
// Change password
|
||||
it('change password fails due to missing current password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ newPassword: 'some wrong password' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('change password fails due to missing new password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('change password fails due to wrong password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: 'some wrong password', newPassword: 'newpassword' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('change password fails due to invalid password', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, newPassword: 'five' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('change password succeeds', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, newPassword: 'new_password' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
200
src/routes/user.js
Normal file
200
src/routes/user.js
Normal file
@@ -0,0 +1,200 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
profile: profile,
|
||||
info: info,
|
||||
update: update,
|
||||
list: listUser,
|
||||
create: createUser,
|
||||
changePassword: changePassword,
|
||||
changeAdmin: changeAdmin,
|
||||
remove: removeUser,
|
||||
verifyPassword: verifyPassword,
|
||||
requireAdmin: requireAdmin
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
user = require('../user.js'),
|
||||
tokendb = require('../tokendb.js'),
|
||||
UserError = user.UserError;
|
||||
|
||||
// http://stackoverflow.com/questions/1497481/javascript-password-generator#1497512
|
||||
function generatePassword() {
|
||||
var length = 8,
|
||||
charset = 'abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
|
||||
retVal = '';
|
||||
for (var i = 0, n = charset.length; i < length; ++i) {
|
||||
retVal += charset.charAt(Math.floor(Math.random() * n));
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
function profile(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
var result = {};
|
||||
result.id = req.user.id;
|
||||
result.tokenType = req.user.tokenType;
|
||||
|
||||
if (req.user.tokenType === tokendb.TYPE_USER || req.user.tokenType === tokendb.TYPE_DEV) {
|
||||
result.username = req.user.username;
|
||||
result.email = req.user.email;
|
||||
result.admin = req.user.admin;
|
||||
}
|
||||
|
||||
next(new HttpSuccess(200, result));
|
||||
}
|
||||
|
||||
function createUser(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
|
||||
var username = req.body.username;
|
||||
var password = generatePassword();
|
||||
var email = req.body.email;
|
||||
|
||||
user.create(username, password, email, false /* admin */, req.user /* creator */, function (error, user) {
|
||||
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, 'Invalid username'));
|
||||
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, 'Invalid email'));
|
||||
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, 'Invalid password'));
|
||||
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
var userInfo = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
admin: user.admin,
|
||||
resetToken: user.resetToken
|
||||
};
|
||||
|
||||
next(new HttpSuccess(201, { userInfo: userInfo }));
|
||||
});
|
||||
}
|
||||
|
||||
function update(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
|
||||
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
|
||||
|
||||
user.update(req.user.id, req.user.username, req.body.email, function (error) {
|
||||
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function changeAdmin(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'API call requires a username.'));
|
||||
if (typeof req.body.admin !== 'boolean') return next(new HttpError(400, 'API call requires an admin setting.'));
|
||||
|
||||
user.changeAdmin(req.body.username, req.body.admin, function (error) {
|
||||
if (error && error.reason === UserError.NOT_ALLOWED) return next(new HttpError(403, 'Last admin'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function changePassword(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires the users old password.'));
|
||||
if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'API call requires the users new password.'));
|
||||
|
||||
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
|
||||
|
||||
user.changePassword(req.user.username, req.body.password, req.body.newPassword, function (error) {
|
||||
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Wrong password'));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Wrong password'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function listUser(req, res, next) {
|
||||
user.list(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, { users: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function info(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.userId, 'string');
|
||||
|
||||
user.get(req.params.userId, function (error, result) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {
|
||||
id: result.id,
|
||||
username: result.username,
|
||||
email: result.email,
|
||||
admin: result.admin
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function removeUser(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.userId, 'string');
|
||||
|
||||
// rules:
|
||||
// - admin can remove any user
|
||||
// - admin cannot remove admin
|
||||
// - user cannot remove himself <- TODO should this actually work?
|
||||
|
||||
if (req.user.id === req.params.userId) return next(new HttpError(403, 'Not allowed to remove yourself.'));
|
||||
|
||||
user.remove(req.params.userId, function (error) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function verifyPassword(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
// developers are allowed to through without password
|
||||
if (req.user.tokenType === tokendb.TYPE_DEV) return next();
|
||||
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
|
||||
|
||||
user.verify(req.user.username, req.body.password, function (error) {
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect'));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Password incorrect'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Middleware which makes the route only accessable for the admin user.
|
||||
*/
|
||||
function requireAdmin(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
if (!req.user.admin) return next(new HttpError(403, 'API call requires admin rights.'));
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
50
src/scripts/backupapp.sh
Executable file
50
src/scripts/backupapp.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This script should be run as root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ $# -lt 3 ]; then
|
||||
echo "Usage: backup.sh <appid> <url> <key>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly DATA_DIR="${HOME}/data"
|
||||
|
||||
app_id="$1"
|
||||
backup_url="$2"
|
||||
backup_key="$3"
|
||||
readonly now=$(date "+%Y-%m-%dT%H:%M:%S")
|
||||
readonly app_data_dir="${DATA_DIR}/${app_id}"
|
||||
readonly app_data_snapshot="${DATA_DIR}/snapshots/${app_id}-${now}"
|
||||
|
||||
btrfs subvolume snapshot -r "${app_data_dir}" "${app_data_snapshot}"
|
||||
|
||||
for try in `seq 1 5`; do
|
||||
echo "Uploading backup to ${backup_url} (try ${try})"
|
||||
error_log=$(mktemp)
|
||||
if tar -cvzf - -C "${app_data_snapshot}" . \
|
||||
| openssl aes-256-cbc -e -pass "pass:${backup_key}" \
|
||||
| curl --fail -H "Content-Type:" -X PUT --data-binary @- "${backup_url}" 2>"${error_log}"; then
|
||||
break
|
||||
fi
|
||||
cat "${error_log}" && rm "${error_log}"
|
||||
done
|
||||
|
||||
btrfs subvolume delete "${app_data_snapshot}"
|
||||
|
||||
if [[ ${try} -eq 5 ]]; then
|
||||
echo "Backup failed"
|
||||
exit 1
|
||||
else
|
||||
echo "Backup successful"
|
||||
fi
|
||||
|
||||
52
src/scripts/backupbox.sh
Executable file
52
src/scripts/backupbox.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This script should be run as root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: backupbox.sh <url> <key>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
backup_url="$1"
|
||||
backup_key="$2"
|
||||
now=$(date "+%Y-%m-%dT%H:%M:%S")
|
||||
BOX_DATA_DIR="${HOME}/data/box"
|
||||
box_snapshot_dir="${HOME}/data/snapshots/box-${now}"
|
||||
|
||||
echo "Creating MySQL dump"
|
||||
mysqldump -u root -ppassword --single-transaction --routines --triggers box > "${BOX_DATA_DIR}/box.mysqldump"
|
||||
|
||||
echo "Snapshoting backup as backup-${now}"
|
||||
btrfs subvolume snapshot -r "${BOX_DATA_DIR}" "${box_snapshot_dir}"
|
||||
|
||||
for try in `seq 1 5`; do
|
||||
echo "Uploading backup to ${backup_url} (try ${try})"
|
||||
error_log=$(mktemp)
|
||||
if tar -cvzf - -C "${box_snapshot_dir}" . \
|
||||
| openssl aes-256-cbc -e -pass "pass:${backup_key}" \
|
||||
| curl --fail -H "Content-Type:" -X PUT --data-binary @- "${backup_url}" 2>"${error_log}"; then
|
||||
break
|
||||
fi
|
||||
cat "${error_log}" && rm "${error_log}"
|
||||
done
|
||||
|
||||
echo "Deleting backup snapshot"
|
||||
btrfs subvolume delete "${box_snapshot_dir}"
|
||||
|
||||
if [[ ${try} -eq 5 ]]; then
|
||||
echo "Backup failed"
|
||||
exit 1
|
||||
else
|
||||
echo "Backup successful"
|
||||
fi
|
||||
|
||||
41
src/scripts/backupswap.sh
Executable file
41
src/scripts/backupswap.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BACKUP_SWAP_FILE="/backup.swap"
|
||||
|
||||
if [[ "$1" == "--on" ]]; then
|
||||
echo "Mounting backup swap"
|
||||
|
||||
if ! swapon -s | grep -q "${BACKUP_SWAP_FILE}"; then
|
||||
swapon "${BACKUP_SWAP_FILE}"
|
||||
else
|
||||
echo "Backup swap already mounted"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--off" ]]; then
|
||||
echo "Unmounting backup swap"
|
||||
|
||||
if swapon -s | grep -q "${BACKUP_SWAP_FILE}"; then
|
||||
swapoff "${BACKUP_SWAP_FILE}"
|
||||
else
|
||||
echo "Backup swap was not mounted"
|
||||
fi
|
||||
fi
|
||||
|
||||
30
src/scripts/createappdir.sh
Executable file
30
src/scripts/createappdir.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${NODE_ENV}" == "cloudron" ]]; then
|
||||
readonly app_data_dir="${HOME}/data/$1"
|
||||
btrfs subvolume create "${app_data_dir}"
|
||||
mkdir -p "${app_data_dir}/data"
|
||||
chown -R yellowtent:yellowtent "${app_data_dir}"
|
||||
else
|
||||
readonly app_data_dir="${HOME}/.cloudron_test/data/$1"
|
||||
mkdir -p "${app_data_dir}/data"
|
||||
chown -R ${SUDO_USER}:${SUDO_USER} "${app_data_dir}"
|
||||
fi
|
||||
|
||||
18
src/scripts/reboot.sh
Executable file
18
src/scripts/reboot.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${NODE_ENV}" == "cloudron" ]]; then
|
||||
shutdown -r now
|
||||
fi
|
||||
|
||||
18
src/scripts/reloadcollectd.sh
Executable file
18
src/scripts/reloadcollectd.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${NODE_ENV}" == "cloudron" ]]; then
|
||||
/etc/init.d/collectd restart
|
||||
fi
|
||||
|
||||
23
src/scripts/reloadnginx.sh
Executable file
23
src/scripts/reloadnginx.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${OSTYPE}" == "darwin"* ]]; then
|
||||
# On Mac, brew installs supervisor in /usr/local/bin
|
||||
export PATH=$PATH:/usr/local/bin
|
||||
fi
|
||||
|
||||
if [[ "${NODE_ENV}" == "cloudron" ]]; then
|
||||
nginx -s reload
|
||||
fi
|
||||
|
||||
48
src/scripts/restoreapp.sh
Executable file
48
src/scripts/restoreapp.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This script should be run as root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ $# -lt 3 ]; then
|
||||
echo "Usage: restoreapp.sh <appid> <url> <key>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly DATA_DIR="${HOME}/data"
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
app_id="$1"
|
||||
restore_url="$2"
|
||||
restore_key="$3"
|
||||
|
||||
echo "Downloading backup: ${restore_url} and key: ${restore_key}"
|
||||
|
||||
for try in `seq 1 5`; do
|
||||
echo "Download backup from ${restore_url} (try ${try})"
|
||||
error_log=$(mktemp)
|
||||
|
||||
if $curl -L "${restore_url}" \
|
||||
| openssl aes-256-cbc -d -pass "pass:${restore_key}" \
|
||||
| tar -zxf - -C "${DATA_DIR}/${app_id}" 2>"${error_log}"; then
|
||||
chown -R yellowtent:yellowtent "${DATA_DIR}/${app_id}"
|
||||
break
|
||||
fi
|
||||
cat "${error_log}" && rm "${error_log}"
|
||||
done
|
||||
|
||||
if [[ ${try} -eq 5 ]]; then
|
||||
echo "restore failed"
|
||||
exit 1
|
||||
else
|
||||
echo "restore successful"
|
||||
fi
|
||||
|
||||
30
src/scripts/rmappdir.sh
Executable file
30
src/scripts/rmappdir.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${NODE_ENV}" == "cloudron" ]]; then
|
||||
readonly app_data_dir="${HOME}/data/$1"
|
||||
if [[ -d "${app_data_dir}" ]]; then
|
||||
find "${app_data_dir}" -mindepth 1 -delete
|
||||
rm -rf "${app_data_dir}" || btrfs subvolume delete "${app_data_dir}"
|
||||
fi
|
||||
else
|
||||
readonly app_data_dir="${HOME}/.cloudron_test/data/$1"
|
||||
rm -rf "${app_data_dir}"
|
||||
fi
|
||||
|
||||
267
src/server.js
Normal file
267
src/server.js
Normal file
@@ -0,0 +1,267 @@
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
auth = require('./auth.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
cron = require('./cron.js'),
|
||||
config = require('./config.js'),
|
||||
database = require('./database.js'),
|
||||
express = require('express'),
|
||||
http = require('http'),
|
||||
mailer = require('./mailer.js'),
|
||||
middleware = require('./middleware'),
|
||||
passport = require('passport'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
routes = require('./routes/index.js'),
|
||||
taskmanager = require('./taskmanager.js');
|
||||
|
||||
var gHttpServer = null;
|
||||
var gInternalHttpServer = null;
|
||||
|
||||
function initializeExpressSync() {
|
||||
var app = express();
|
||||
var httpServer = http.createServer(app);
|
||||
|
||||
var QUERY_LIMIT = '10mb', // max size for json and urlencoded queries
|
||||
FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart
|
||||
|
||||
var REQUEST_TIMEOUT = 10000; // timeout for all requests
|
||||
|
||||
var json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
|
||||
urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded
|
||||
|
||||
app.set('views', path.join(__dirname, 'oauth2views'));
|
||||
app.set('view options', { layout: true, debug: true });
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
app.use(express.static(path.join(__dirname, '/../webadmin')));
|
||||
} else {
|
||||
app.use(middleware.morgan('dev', { immediate: false }));
|
||||
}
|
||||
|
||||
var router = new express.Router();
|
||||
router.del = router.delete; // amend router.del for readability further on
|
||||
|
||||
app
|
||||
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||||
.use(json)
|
||||
.use(urlencoded)
|
||||
.use(middleware.cookieParser())
|
||||
.use(middleware.favicon(paths.FAVICON_FILE)) // used when serving oauth login page
|
||||
.use(middleware.cors({ origins: [ '*' ], allowCredentials: true }))
|
||||
.use(middleware.session({ secret: 'yellow is blue', resave: true, saveUninitialized: true, cookie: { path: '/', httpOnly: true, secure: false, maxAge: 600000 } }))
|
||||
.use(passport.initialize())
|
||||
.use(passport.session())
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
|
||||
// NOTE: these limits have to be in sync with nginx limits
|
||||
var FILE_SIZE_LIMIT = '1mb', // max file size that can be uploaded
|
||||
FILE_TIMEOUT = 60 * 1000; // increased timeout for file uploads (1 min)
|
||||
|
||||
var multipart = middleware.multipart({ maxFieldsSize: FIELD_LIMIT, limit: FILE_SIZE_LIMIT, timeout: FILE_TIMEOUT });
|
||||
|
||||
// scope middleware implicitly also adds bearer token verification
|
||||
var rootScope = routes.oauth2.scope('root');
|
||||
var profileScope = routes.oauth2.scope('profile');
|
||||
var usersScope = routes.oauth2.scope('users');
|
||||
var appsScope = routes.oauth2.scope('apps');
|
||||
var developerScope = routes.oauth2.scope('developer');
|
||||
var settingsScope = routes.oauth2.scope('settings');
|
||||
|
||||
// csrf protection
|
||||
var csrf = routes.oauth2.csrf;
|
||||
|
||||
// public routes
|
||||
router.post('/api/v1/cloudron/activate', routes.cloudron.setupTokenAuth, routes.cloudron.activate);
|
||||
router.get ('/api/v1/cloudron/progress', routes.cloudron.getProgress);
|
||||
router.get ('/api/v1/cloudron/status', routes.cloudron.getStatus);
|
||||
router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar
|
||||
|
||||
// developer routes
|
||||
router.post('/api/v1/developer', developerScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.developer.setEnabled);
|
||||
router.get ('/api/v1/developer', developerScope, routes.developer.enabled, routes.developer.status);
|
||||
router.post('/api/v1/developer/login', routes.developer.enabled, routes.developer.login);
|
||||
|
||||
// private routes
|
||||
router.get ('/api/v1/cloudron/config', rootScope, routes.cloudron.getConfig);
|
||||
router.post('/api/v1/cloudron/update', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
|
||||
router.get ('/api/v1/cloudron/reboot', rootScope, routes.cloudron.reboot);
|
||||
router.post('/api/v1/cloudron/migrate', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
|
||||
router.post('/api/v1/cloudron/certificate', rootScope, multipart, routes.cloudron.setCertificate);
|
||||
router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
|
||||
|
||||
router.get ('/api/v1/profile', profileScope, routes.user.profile);
|
||||
|
||||
router.get ('/api/v1/users', usersScope, routes.user.list);
|
||||
router.post('/api/v1/users', usersScope, routes.user.requireAdmin, routes.user.create);
|
||||
router.get ('/api/v1/users/:userId', usersScope, routes.user.info);
|
||||
router.put ('/api/v1/users/:userId', usersScope, routes.user.verifyPassword, routes.user.update);
|
||||
router.del ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.user.remove);
|
||||
router.post('/api/v1/users/:userId/password', usersScope, routes.user.changePassword); // changePassword verifies password
|
||||
router.post('/api/v1/users/:userId/admin', usersScope, routes.user.requireAdmin, routes.user.changeAdmin);
|
||||
|
||||
// form based login routes used by oauth2 frame
|
||||
router.get ('/api/v1/session/login', csrf, routes.oauth2.loginForm);
|
||||
router.post('/api/v1/session/login', csrf, routes.oauth2.login);
|
||||
router.get ('/api/v1/session/logout', routes.oauth2.logout);
|
||||
router.get ('/api/v1/session/callback', routes.oauth2.callback);
|
||||
router.get ('/api/v1/session/error', routes.oauth2.error);
|
||||
router.get ('/api/v1/session/password/resetRequest.html', csrf, routes.oauth2.passwordResetRequestSite);
|
||||
router.post('/api/v1/session/password/resetRequest', csrf, routes.oauth2.passwordResetRequest);
|
||||
router.get ('/api/v1/session/password/sent.html', routes.oauth2.passwordSentSite);
|
||||
router.get ('/api/v1/session/password/setup.html', csrf, routes.oauth2.passwordSetupSite);
|
||||
router.get ('/api/v1/session/password/reset.html', csrf, routes.oauth2.passwordResetSite);
|
||||
router.post('/api/v1/session/password/reset', csrf, routes.oauth2.passwordReset);
|
||||
|
||||
// oauth2 routes
|
||||
router.get ('/api/v1/oauth/dialog/authorize', routes.oauth2.authorization);
|
||||
router.post('/api/v1/oauth/dialog/authorize/decision', csrf, routes.oauth2.decision);
|
||||
router.post('/api/v1/oauth/token', routes.oauth2.token);
|
||||
router.get ('/api/v1/oauth/clients', settingsScope, routes.clients.getAllByUserId);
|
||||
router.post('/api/v1/oauth/clients', routes.developer.enabled, settingsScope, routes.clients.add);
|
||||
router.get ('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.get);
|
||||
router.post('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.add);
|
||||
router.put ('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.update);
|
||||
router.del ('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.del);
|
||||
router.get ('/api/v1/oauth/clients/:clientId/tokens', settingsScope, routes.clients.getClientTokens);
|
||||
router.del ('/api/v1/oauth/clients/:clientId/tokens', settingsScope, routes.clients.delClientTokens);
|
||||
|
||||
// app routes
|
||||
router.get ('/api/v1/apps', appsScope, routes.apps.getApps);
|
||||
router.get ('/api/v1/apps/:id', appsScope, routes.apps.getApp);
|
||||
router.get ('/api/v1/apps/:id/icon', appsScope, routes.apps.getAppIcon);
|
||||
|
||||
router.post('/api/v1/apps/install', appsScope, routes.user.requireAdmin, routes.apps.installApp);
|
||||
router.post('/api/v1/apps/:id/uninstall', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.uninstallApp);
|
||||
router.post('/api/v1/apps/:id/configure', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.configureApp);
|
||||
router.post('/api/v1/apps/:id/update', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.updateApp);
|
||||
router.post('/api/v1/apps/:id/restore', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.restoreApp);
|
||||
router.post('/api/v1/apps/:id/backup', appsScope, routes.user.requireAdmin, routes.apps.backupApp);
|
||||
router.post('/api/v1/apps/:id/stop', appsScope, routes.user.requireAdmin, routes.apps.stopApp);
|
||||
router.post('/api/v1/apps/:id/start', appsScope, routes.user.requireAdmin, routes.apps.startApp);
|
||||
router.get ('/api/v1/apps/:id/logstream', appsScope, routes.user.requireAdmin, routes.apps.getLogStream);
|
||||
router.get ('/api/v1/apps/:id/logs', appsScope, routes.user.requireAdmin, routes.apps.getLogs);
|
||||
router.get ('/api/v1/apps/:id/exec', routes.developer.enabled, appsScope, routes.user.requireAdmin, routes.apps.exec);
|
||||
|
||||
// subdomain routes
|
||||
router.get ('/api/v1/subdomains/:subdomain', routes.apps.getAppBySubdomain);
|
||||
|
||||
// settings routes
|
||||
router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.settings.getAutoupdatePattern);
|
||||
router.post('/api/v1/settings/autoupdate_pattern', settingsScope, routes.settings.setAutoupdatePattern);
|
||||
router.get ('/api/v1/settings/cloudron_name', settingsScope, routes.settings.getCloudronName);
|
||||
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.settings.setCloudronName);
|
||||
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.settings.getCloudronAvatar);
|
||||
router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar);
|
||||
|
||||
// backup routes
|
||||
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
|
||||
router.post('/api/v1/backups', settingsScope, routes.backups.create);
|
||||
|
||||
// upgrade handler
|
||||
httpServer.on('upgrade', function (req, socket, head) {
|
||||
if (req.headers['upgrade'] !== 'tcp') return req.end('Only TCP upgrades are possible');
|
||||
|
||||
// create a node response object for express
|
||||
var res = new http.ServerResponse({});
|
||||
res.assignSocket(socket);
|
||||
res.sendUpgradeHandshake = function () { // could extend express.response as well
|
||||
socket.write('HTTP/1.1 101 TCP Handshake\r\n' +
|
||||
'Upgrade: tcp\r\n' +
|
||||
'Connection: Upgrade\r\n' +
|
||||
'\r\n');
|
||||
};
|
||||
|
||||
// route through express middleware
|
||||
app(req, res, function (error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
function initializeInternalExpressSync() {
|
||||
var app = express();
|
||||
var httpServer = http.createServer(app);
|
||||
|
||||
var QUERY_LIMIT = '10mb'; // max size for json and urlencoded queries
|
||||
var REQUEST_TIMEOUT = 10000; // timeout for all requests
|
||||
|
||||
var json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
|
||||
urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded
|
||||
|
||||
app.use(middleware.morgan('dev', { immediate: false }));
|
||||
|
||||
var router = new express.Router();
|
||||
router.del = router.delete; // amend router.del for readability further on
|
||||
|
||||
app
|
||||
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||||
.use(json)
|
||||
.use(urlencoded)
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
|
||||
// internal routes
|
||||
router.post('/api/v1/backup', routes.internal.backup);
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert.strictEqual(gHttpServer, null, 'Server is already up and running.');
|
||||
|
||||
gHttpServer = initializeExpressSync();
|
||||
gInternalHttpServer = initializeInternalExpressSync();
|
||||
|
||||
async.series([
|
||||
auth.initialize,
|
||||
database.initialize,
|
||||
taskmanager.initialize,
|
||||
cloudron.initialize,
|
||||
mailer.initialize,
|
||||
cron.initialize,
|
||||
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
|
||||
gInternalHttpServer.listen.bind(gInternalHttpServer, config.get('internalPort'), '127.0.0.1')
|
||||
], callback);
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!gHttpServer) return callback(null);
|
||||
|
||||
async.series([
|
||||
auth.uninitialize,
|
||||
cloudron.uninitialize,
|
||||
taskmanager.uninitialize,
|
||||
cron.uninitialize,
|
||||
mailer.uninitialize,
|
||||
database.uninitialize,
|
||||
gHttpServer.close.bind(gHttpServer),
|
||||
gInternalHttpServer.close.bind(gInternalHttpServer)
|
||||
], function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
gHttpServer = null;
|
||||
gInternalHttpServer = null;
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
199
src/settings.js
Normal file
199
src/settings.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
SettingsError: SettingsError,
|
||||
|
||||
getAutoupdatePattern: getAutoupdatePattern,
|
||||
setAutoupdatePattern: setAutoupdatePattern,
|
||||
|
||||
getTimeZone: getTimeZone,
|
||||
setTimeZone: setTimeZone,
|
||||
|
||||
getCloudronName: getCloudronName,
|
||||
setCloudronName: setCloudronName,
|
||||
|
||||
getCloudronAvatar: getCloudronAvatar,
|
||||
setCloudronAvatar: setCloudronAvatar,
|
||||
|
||||
getDefaultSync: getDefaultSync,
|
||||
getAll: getAll,
|
||||
|
||||
AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern',
|
||||
TIME_ZONE_KEY: 'time_zone',
|
||||
CLOUDRON_NAME_KEY: 'cloudron_name',
|
||||
|
||||
events: new (require('events').EventEmitter)()
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settingsdb = require('./settingsdb.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var gDefaults = (function () {
|
||||
var tz = safe.fs.readFileSync('/etc/timezone', 'utf8');
|
||||
tz = tz ? tz.trim() : 'America/Los_Angeles';
|
||||
|
||||
var result = { };
|
||||
result[exports.AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
|
||||
result[exports.TIME_ZONE_KEY] = tz;
|
||||
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
|
||||
|
||||
return result;
|
||||
})();
|
||||
|
||||
if (config.TEST) {
|
||||
// avoid noisy warnings during npm test
|
||||
exports.events.setMaxListeners(100);
|
||||
}
|
||||
|
||||
function SettingsError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(SettingsError, Error);
|
||||
SettingsError.INTERNAL_ERROR = 'Internal Error';
|
||||
SettingsError.NOT_FOUND = 'Not Found';
|
||||
SettingsError.BAD_FIELD = 'Bad Field';
|
||||
|
||||
function setAutoupdatePattern(pattern, callback) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (pattern !== 'never') { // check if pattern is valid
|
||||
var job = safe.safeCall(function () { return new CronJob(pattern); });
|
||||
if (!job) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Invalid pattern'));
|
||||
}
|
||||
|
||||
settingsdb.set(exports.AUTOUPDATE_PATTERN_KEY, pattern, function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.AUTOUPDATE_PATTERN_KEY, pattern);
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getAutoupdatePattern(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.AUTOUPDATE_PATTERN_KEY, function (error, pattern) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.AUTOUPDATE_PATTERN_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, pattern);
|
||||
});
|
||||
}
|
||||
|
||||
function setTimeZone(tz, callback) {
|
||||
assert.strictEqual(typeof tz, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.set(exports.TIME_ZONE_KEY, tz, function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.TIME_ZONE_KEY, tz);
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getTimeZone(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.TIME_ZONE_KEY, function (error, tz) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.AUTOUPDATE_PATTERN_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, tz);
|
||||
});
|
||||
}
|
||||
|
||||
function getCloudronName(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.CLOUDRON_NAME_KEY, function (error, name) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_NAME_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
callback(null, name);
|
||||
});
|
||||
}
|
||||
|
||||
function setCloudronName(name, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!name) return callback(new SettingsError(SettingsError.BAD_FIELD));
|
||||
|
||||
settingsdb.set(exports.CLOUDRON_NAME_KEY, name, function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.CLOUDRON_NAME_KEY, name);
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getCloudronAvatar(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var avatar = safe.fs.readFileSync(paths.CLOUDRON_AVATAR_FILE);
|
||||
if (avatar) return callback(null, avatar);
|
||||
|
||||
// try default fallback
|
||||
avatar = safe.fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE);
|
||||
if (avatar) return callback(null, avatar);
|
||||
|
||||
callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error));
|
||||
}
|
||||
|
||||
function setCloudronAvatar(avatar, callback) {
|
||||
assert(util.isBuffer(avatar));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.CLOUDRON_AVATAR_FILE, avatar)) {
|
||||
return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error));
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function getDefaultSync(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
return gDefaults[name];
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.getAll(function (error, settings) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
var result = _.extend({ }, gDefaults);
|
||||
settings.forEach(function (setting) { result[setting.name] = setting.value; });
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
54
src/settingsdb.js
Normal file
54
src/settingsdb.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getAll: getAll,
|
||||
set: set,
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
|
||||
function get(key, callback) {
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT * FROM settings WHERE name = ?', [ key ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, result[0].value);
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
database.query('SELECT * FROM settings ORDER BY name', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function set(key, value, callback) {
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert(value === null || typeof value === 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO settings (name, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value)', [ key, value ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); // don't rely on affectedRows here since it gives 2
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
database.query('DELETE FROM settings', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
58
src/shell.js
Normal file
58
src/shell.js
Normal file
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
sudo: sudo,
|
||||
exec: exec
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
child_process = require('child_process'),
|
||||
debug = require('debug')('box:shell.js'),
|
||||
once = require('once'),
|
||||
util = require('util');
|
||||
|
||||
var SUDO = '/usr/bin/sudo';
|
||||
|
||||
function exec(tag, file, args, callback) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof file, 'string');
|
||||
assert(util.isArray(args));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback = once(callback); // exit may or may not be called after an 'error'
|
||||
|
||||
debug(tag + ' execFile: %s %s', file, args.join(' '));
|
||||
|
||||
var cp = child_process.spawn(file, args);
|
||||
cp.stdout.on('data', function (data) {
|
||||
debug(tag + ' (stdout): %s', data.toString('utf8'));
|
||||
});
|
||||
|
||||
cp.stderr.on('data', function (data) {
|
||||
debug(tag + ' (stderr): %s', data.toString('utf8'));
|
||||
});
|
||||
|
||||
cp.on('exit', function (code, signal) {
|
||||
if (code || signal) debug(tag + ' code: %s, signal: %s', code, signal);
|
||||
|
||||
callback(code === 0 ? null : new Error(util.format('Exited with error %s signal %s', code, signal)));
|
||||
});
|
||||
|
||||
cp.on('error', function (error) {
|
||||
debug(tag + ' code: %s, signal: %s', error.code, error.signal);
|
||||
callback(error);
|
||||
});
|
||||
|
||||
return cp;
|
||||
}
|
||||
|
||||
function sudo(tag, args, callback) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert(util.isArray(args));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// -S makes sudo read stdin for password
|
||||
var cp = exec(tag, SUDO, [ '-S' ].concat(args), callback);
|
||||
cp.stdin.end();
|
||||
}
|
||||
|
||||
28
src/sysinfo.js
Normal file
28
src/sysinfo.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getIp: getIp
|
||||
};
|
||||
|
||||
var os = require('os');
|
||||
|
||||
var gCachedIp = null;
|
||||
|
||||
function getIp() {
|
||||
if (gCachedIp) return gCachedIp;
|
||||
|
||||
var ifaces = os.networkInterfaces();
|
||||
for (var dev in ifaces) {
|
||||
if (dev.match(/^(en|eth|wlp).*/) === null) continue;
|
||||
|
||||
for (var i = 0; i < ifaces[dev].length; i++) {
|
||||
if (ifaces[dev][i].family === 'IPv4') {
|
||||
gCachedIp = ifaces[dev][i].address;
|
||||
return gCachedIp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
102
src/taskmanager.js
Normal file
102
src/taskmanager.js
Normal file
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
|
||||
restartAppTask: restartAppTask
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
child_process = require('child_process'),
|
||||
debug = require('debug')('box:taskmanager'),
|
||||
locker = require('./locker.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
var gActiveTasks = { };
|
||||
var gPendingTasks = [ ];
|
||||
|
||||
// Task concurrency is 1 for two reasons:
|
||||
// 1. The backup scripts (app and box) turn off swap after finish disregarding other backup processes
|
||||
// 2. apptask getFreePort has race with multiprocess
|
||||
var TASK_CONCURRENCY = 1;
|
||||
var NOOP_CALLBACK = function (error) { console.error(error); };
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// resume app installs and uninstalls
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
apps.forEach(function (app) {
|
||||
debug('Creating process for %s (%s) with state %s', app.location, app.id, app.installationState);
|
||||
startAppTask(app.id);
|
||||
});
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
locker.on('unlocked', startNextTask);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gPendingTasks = [ ]; // clear this first, otherwise stopAppTask will resume them
|
||||
for (var appId in gActiveTasks) {
|
||||
stopAppTask(appId);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function startNextTask() {
|
||||
if (gPendingTasks.length === 0) return;
|
||||
assert(Object.keys(gActiveTasks).length === 0); // since we allow only one task at a time
|
||||
|
||||
startAppTask(gPendingTasks.shift());
|
||||
}
|
||||
|
||||
function startAppTask(appId) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert(!(appId in gActiveTasks));
|
||||
|
||||
var lockError = locker.lock(locker.OP_APPTASK);
|
||||
|
||||
if (lockError || Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
|
||||
debug('Reached concurrency limit, queueing task for %s', appId);
|
||||
gPendingTasks.push(appId);
|
||||
return;
|
||||
}
|
||||
|
||||
gActiveTasks[appId] = child_process.fork(__dirname + '/apptask.js', [ appId ]);
|
||||
gActiveTasks[appId].once('exit', function (code) {
|
||||
debug('Task for %s completed with status %s', appId, code);
|
||||
if (code && code !== 50) { // apptask crashed
|
||||
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code }, NOOP_CALLBACK);
|
||||
}
|
||||
delete gActiveTasks[appId];
|
||||
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
|
||||
});
|
||||
}
|
||||
|
||||
function stopAppTask(appId) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
|
||||
if (gActiveTasks[appId]) {
|
||||
debug('stopAppTask : Killing existing task of %s with pid %s: ', appId, gActiveTasks[appId].pid);
|
||||
gActiveTasks[appId].kill(); // this will end up calling the 'exit' handler
|
||||
delete gActiveTasks[appId];
|
||||
} else if (gPendingTasks.indexOf(appId) !== -1) {
|
||||
debug('stopAppTask: Removing existing pending task : %s', appId);
|
||||
gPendingTasks = _.without(gPendingTasks, appId);
|
||||
}
|
||||
}
|
||||
|
||||
function restartAppTask(appId) {
|
||||
stopAppTask(appId);
|
||||
startAppTask(appId);
|
||||
}
|
||||
|
||||
161
src/test/apps-test.js
Normal file
161
src/test/apps-test.js
Normal file
@@ -0,0 +1,161 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var appdb = require('../appdb.js'),
|
||||
apps = require('../apps.js'),
|
||||
AppsError = apps.AppsError,
|
||||
async = require('async'),
|
||||
config = require('../config.js'),
|
||||
constants = require('../constants.js'),
|
||||
database = require('../database.js'),
|
||||
expect = require('expect.js');
|
||||
|
||||
describe('Apps', function () {
|
||||
var APP_0 = {
|
||||
id: 'appid-0',
|
||||
appStoreId: 'appStoreId-0',
|
||||
installationState: appdb.ISTATE_PENDING_INSTALL,
|
||||
installationProgress: null,
|
||||
runState: null,
|
||||
location: 'some-location-0',
|
||||
manifest: {
|
||||
version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0',
|
||||
tcpPorts: {
|
||||
PORT: {
|
||||
description: 'this is a port that i expose',
|
||||
containerPort: '1234'
|
||||
}
|
||||
}
|
||||
},
|
||||
httpPort: null,
|
||||
containerId: null,
|
||||
portBindings: { PORT: 5678 },
|
||||
healthy: null,
|
||||
accessRestriction: ''
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear,
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction)
|
||||
], done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
database._clear(done);
|
||||
});
|
||||
|
||||
describe('validateHostname', function () {
|
||||
it('does not allow admin subdomain', function () {
|
||||
expect(apps._validateHostname(constants.ADMIN_LOCATION, 'cloudron.us')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('cannot have >63 length subdomains', function () {
|
||||
var s = '';
|
||||
for (var i = 0; i < 64; i++) s += 's';
|
||||
expect(apps._validateHostname(s, 'cloudron.us')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows only alphanumerics and hypen', function () {
|
||||
expect(apps._validateHostname('#2r', 'cloudron.us')).to.be.an(Error);
|
||||
expect(apps._validateHostname('a%b', 'cloudron.us')).to.be.an(Error);
|
||||
expect(apps._validateHostname('ab_', 'cloudron.us')).to.be.an(Error);
|
||||
expect(apps._validateHostname('a.b', 'cloudron.us')).to.be.an(Error);
|
||||
expect(apps._validateHostname('-ab', 'cloudron.us')).to.be.an(Error);
|
||||
expect(apps._validateHostname('ab-', 'cloudron.us')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('total length cannot exceed 255', function () {
|
||||
var s = '';
|
||||
for (var i = 0; i < (255 - 'cloudron.us'.length); i++) s += 's';
|
||||
|
||||
expect(apps._validateHostname(s, 'cloudron.us')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allow valid domains', function () {
|
||||
expect(apps._validateHostname('a', 'cloudron.us')).to.be(null);
|
||||
expect(apps._validateHostname('a0-x', 'cloudron.us')).to.be(null);
|
||||
expect(apps._validateHostname('01', 'cloudron.us')).to.be(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePortBindings', function () {
|
||||
it('does not allow invalid host port', function () {
|
||||
expect(apps._validatePortBindings({ port: -1 })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 0 })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 'text' })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 65536 })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 1024 })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow ports not as part of manifest', function () {
|
||||
expect(apps._validatePortBindings({ port: 1567 })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 1567 }, { port3: null })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows valid bindings', function () {
|
||||
expect(apps._validatePortBindings({ port: 1025 }, { port: null })).to.be(null);
|
||||
expect(apps._validatePortBindings({
|
||||
port1: 4033,
|
||||
port2: 3242,
|
||||
port3: 1234
|
||||
}, { port1: null, port2: null, port3: null })).to.be(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getters', function () {
|
||||
it('cannot get invalid app', function (done) {
|
||||
apps.get('nope', function (error, app) {
|
||||
expect(error).to.be.ok();
|
||||
expect(error.reason).to.be(AppsError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get valid app', function (done) {
|
||||
apps.get(APP_0.id, function (error, app) {
|
||||
expect(error).to.be(null);
|
||||
expect(app).to.be.ok();
|
||||
expect(app.iconUrl).to.be(null);
|
||||
expect(app.fqdn).to.eql(APP_0.location + '-' + config.fqdn());
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot getBySubdomain', function (done) {
|
||||
apps.getBySubdomain('moang', function (error, app) {
|
||||
expect(error).to.be.ok();
|
||||
expect(error.reason).to.be(AppsError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can getBySubdomain', function (done) {
|
||||
apps.getBySubdomain(APP_0.location, function (error, app) {
|
||||
expect(error).to.be(null);
|
||||
expect(app).to.be.ok();
|
||||
expect(app.iconUrl).to.eql(null);
|
||||
expect(app.fqdn).to.eql(APP_0.location + '-' + config.fqdn());
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can getAll', function (done) {
|
||||
apps.getAll(function (error, apps) {
|
||||
expect(error).to.be(null);
|
||||
expect(apps).to.be.an(Array);
|
||||
expect(apps[0].id).to.be(APP_0.id);
|
||||
expect(apps[0].iconUrl).to.be(null);
|
||||
expect(apps[0].fqdn).to.eql(APP_0.location + '-' + config.fqdn());
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
212
src/test/apptask-test.js
Normal file
212
src/test/apptask-test.js
Normal file
@@ -0,0 +1,212 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var addons = require('../addons.js'),
|
||||
appdb = require('../appdb.js'),
|
||||
apptask = require('../apptask.js'),
|
||||
config = require('../config.js'),
|
||||
database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
net = require('net'),
|
||||
nock = require('nock'),
|
||||
paths = require('../paths.js'),
|
||||
sysinfo = require('../sysinfo.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
var MANIFEST = {
|
||||
"id": "io.cloudron.test",
|
||||
"author": "The Presidents Of the United States Of America",
|
||||
"title": "test title",
|
||||
"description": "test description",
|
||||
"tagline": "test rocks",
|
||||
"website": "http://test.cloudron.io",
|
||||
"contactEmail": "support@cloudron.io",
|
||||
"version": "0.1.0",
|
||||
"manifestVersion": 1,
|
||||
"dockerImage": "girish/test:0.2.0",
|
||||
"healthCheckPath": "/",
|
||||
"httpPort": 7777,
|
||||
"tcpPorts": {
|
||||
"ECHO_SERVER_PORT": {
|
||||
"title": "Echo Server Port",
|
||||
"description": "Echo server",
|
||||
"containerPort": 7778
|
||||
}
|
||||
},
|
||||
"addons": {
|
||||
"oauth": { },
|
||||
"redis": { },
|
||||
"mysql": { },
|
||||
"postgresql": { }
|
||||
}
|
||||
};
|
||||
|
||||
var APP = {
|
||||
id: 'appid',
|
||||
appStoreId: 'appStoreId',
|
||||
installationState: appdb.ISTATE_PENDING_INSTALL,
|
||||
runState: null,
|
||||
location: 'applocation',
|
||||
manifest: MANIFEST,
|
||||
containerId: null,
|
||||
httpPort: 4567,
|
||||
portBindings: null,
|
||||
accessRestriction: '',
|
||||
dnsRecordId: 'someDnsRecordId'
|
||||
};
|
||||
|
||||
describe('apptask', function () {
|
||||
before(function (done) {
|
||||
config.set('version', '0.5.0');
|
||||
database.initialize(function (error) {
|
||||
expect(error).to.be(null);
|
||||
appdb.add(APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, done);
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
database._clear(done);
|
||||
});
|
||||
|
||||
it('initializes succesfully', function (done) {
|
||||
apptask.initialize(done);
|
||||
});
|
||||
|
||||
it('free port', function (done) {
|
||||
apptask._getFreePort(function (error, port) {
|
||||
expect(error).to.be(null);
|
||||
expect(port).to.be.a('number');
|
||||
var client = net.connect(port);
|
||||
client.on('connect', function () { done(new Error('Port is not free:' + port)); });
|
||||
client.on('error', function (error) { done(); });
|
||||
});
|
||||
});
|
||||
|
||||
it('configure nginx correctly', function (done) {
|
||||
apptask._configureNginx(APP, function (error) {
|
||||
expect(fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + APP.id + '.conf'));
|
||||
// expect(error).to.be(null); // this fails because nginx cannot be restarted
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('unconfigure nginx', function (done) {
|
||||
apptask._unconfigureNginx(APP, function (error) {
|
||||
expect(!fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + APP.id + '.conf'));
|
||||
// expect(error).to.be(null); // this fails because nginx cannot be restarted
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create volume', function (done) {
|
||||
apptask._createVolume(APP, function (error) {
|
||||
expect(fs.existsSync(paths.DATA_DIR + '/' + APP.id + '/data')).to.be(true);
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('delete volume', function (done) {
|
||||
apptask._deleteVolume(APP, function (error) {
|
||||
expect(!fs.existsSync(paths.DATA_DIR + '/' + APP.id + '/data')).to.be(true);
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allocate OAuth credentials', function (done) {
|
||||
addons._allocateOAuthCredentials(APP, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('remove OAuth credentials', function (done) {
|
||||
addons._removeOAuthCredentials(APP, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('remove OAuth credentials twice succeeds', function (done) {
|
||||
addons._removeOAuthCredentials(APP, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('barfs on empty manifest', function (done) {
|
||||
var badApp = _.extend({ }, APP);
|
||||
badApp.manifest = { };
|
||||
|
||||
apptask._verifyManifest(badApp, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('barfs on bad manifest', function (done) {
|
||||
var badApp = _.extend({ }, APP);
|
||||
badApp.manifest = _.extend({ }, APP.manifest);
|
||||
delete badApp.manifest['id'];
|
||||
|
||||
apptask._verifyManifest(badApp, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('barfs on incompatible manifest', function (done) {
|
||||
var badApp = _.extend({ }, APP);
|
||||
badApp.manifest = _.extend({ }, APP.manifest);
|
||||
badApp.manifest.maxBoxVersion = '0.0.0'; // max box version is too small
|
||||
|
||||
apptask._verifyManifest(badApp, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('verifies manifest', function (done) {
|
||||
var goodApp = _.extend({ }, APP);
|
||||
|
||||
apptask._verifyManifest(goodApp, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('registers subdomain', function (done) {
|
||||
nock.cleanAll();
|
||||
var scope = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/subdomains?token=' + config.token(), { records: [ { subdomain: APP.location, type: 'A', value: sysinfo.getIp() } ] })
|
||||
.reply(201, { ids: [ APP.dnsRecordId ] });
|
||||
|
||||
apptask._registerSubdomain(APP, function (error) {
|
||||
expect(error).to.be(null);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('unregisters subdomain', function (done) {
|
||||
nock.cleanAll();
|
||||
var scope = nock(config.apiServerOrigin())
|
||||
.delete('/api/v1/subdomains/' + APP.dnsRecordId + '?token=' + config.token())
|
||||
.reply(204, {});
|
||||
|
||||
apptask._unregisterSubdomain(APP, function (error) {
|
||||
expect(error).to.be(null);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
58
src/test/checkInstall
Executable file
58
src/test/checkInstall
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
# reset sudo timestamp to avoid wrong success
|
||||
sudo -k || sudo --reset-timestamp
|
||||
|
||||
# checks if all scripts are sudo access
|
||||
scripts=("${SOURCE_DIR}/scripts/rmappdir.sh" \
|
||||
"${SOURCE_DIR}/scripts/createappdir.sh" \
|
||||
"${SOURCE_DIR}/scripts/reloadnginx.sh" \
|
||||
"${SOURCE_DIR}/scripts/backupbox.sh" \
|
||||
"${SOURCE_DIR}/scripts/backupapp.sh" \
|
||||
"${SOURCE_DIR}/scripts/restoreapp.sh" \
|
||||
"${SOURCE_DIR}/scripts/reboot.sh" \
|
||||
"${SOURCE_DIR}/scripts/backupswap.sh" \
|
||||
"${SOURCE_DIR}/scripts/reloadcollectd.sh")
|
||||
|
||||
for script in "${scripts[@]}"; do
|
||||
if [[ $(sudo -n "${script}" --check 2>/dev/null) != "OK" ]]; then
|
||||
echo ""
|
||||
echo "${script} does not have sudo access."
|
||||
echo "You have to add the lines below to /etc/sudoers.d/yellowtent."
|
||||
echo ""
|
||||
echo "Defaults!${script} env_keep=\"HOME NODE_ENV\""
|
||||
echo "${USER} ALL=(ALL) NOPASSWD: ${script}"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ! docker inspect girish/test:0.2.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull girish/test:0.2.0 for tests to run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker inspect cloudron/redis:0.3.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull cloudron/redis:0.3.0 for tests to run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker inspect cloudron/mysql:0.3.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull cloudron/mysql:0.3.0 for tests to run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker inspect cloudron/postgresql:0.3.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull cloudron/postgresql:0.3.0 for tests to run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker inspect cloudron/mongodb:0.3.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull cloudron/mongodb:0.3.0 for tests to run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
95
src/test/config-test.js
Normal file
95
src/test/config-test.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global after:false */
|
||||
/* global before:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var constants = require('../constants.js'),
|
||||
expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
path = require('path');
|
||||
|
||||
var config = null;
|
||||
|
||||
describe('config', function () {
|
||||
before(function () {
|
||||
delete require.cache[require.resolve('../config.js')];
|
||||
config = require('../config.js');
|
||||
});
|
||||
|
||||
after(function () {
|
||||
delete require.cache[require.resolve('../config.js')];
|
||||
});
|
||||
|
||||
it('baseDir() is set', function (done) {
|
||||
expect(config.baseDir()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
|
||||
it('cloudron.conf generated automatically', function (done) {
|
||||
expect(fs.existsSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).to.be.ok();
|
||||
done();
|
||||
});
|
||||
|
||||
it('did set default values', function () {
|
||||
expect(config.isCustomDomain()).to.equal(false);
|
||||
expect(config.fqdn()).to.equal('localhost');
|
||||
expect(config.adminOrigin()).to.equal('https://' + constants.ADMIN_LOCATION + '-localhost');
|
||||
expect(config.appFqdn('app')).to.equal('app-localhost');
|
||||
expect(config.zoneName()).to.equal('localhost');
|
||||
});
|
||||
|
||||
it('set saves value in file', function (done) {
|
||||
config.set('foobar', 'somevalue');
|
||||
expect(JSON.parse(fs.readFileSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).foobar).to.eql('somevalue');
|
||||
done();
|
||||
});
|
||||
|
||||
it('set - simple key value', function (done) {
|
||||
config.set('foobar', 'somevalue2');
|
||||
expect(config.get('foobar')).to.eql('somevalue2');
|
||||
done();
|
||||
});
|
||||
|
||||
it('set - object', function (done) {
|
||||
config.set( { fqdn: 'something.com' } );
|
||||
expect(config.fqdn()).to.eql('something.com');
|
||||
done();
|
||||
});
|
||||
|
||||
it('uses dotted locations with custom domain', function () {
|
||||
config.set('fqdn', 'example.com');
|
||||
config.set('isCustomDomain', true);
|
||||
|
||||
expect(config.isCustomDomain()).to.equal(true);
|
||||
expect(config.fqdn()).to.equal('example.com');
|
||||
expect(config.adminOrigin()).to.equal('https://' + constants.ADMIN_LOCATION + '.example.com');
|
||||
expect(config.appFqdn('app')).to.equal('app.example.com');
|
||||
expect(config.zoneName()).to.equal('example.com');
|
||||
});
|
||||
|
||||
it('uses hyphen locations with non-custom domain', function () {
|
||||
config.set('fqdn', 'test.example.com');
|
||||
config.set('isCustomDomain', false);
|
||||
|
||||
expect(config.isCustomDomain()).to.equal(false);
|
||||
expect(config.fqdn()).to.equal('test.example.com');
|
||||
expect(config.adminOrigin()).to.equal('https://' + constants.ADMIN_LOCATION + '-test.example.com');
|
||||
expect(config.appFqdn('app')).to.equal('app-test.example.com');
|
||||
expect(config.zoneName()).to.equal('example.com');
|
||||
});
|
||||
|
||||
it('can set arbitrary values', function (done) {
|
||||
config.set('random', 'value');
|
||||
expect(config.get('random')).to.equal('value');
|
||||
|
||||
config.set('this.is.madness', 42);
|
||||
expect(config.get('this.is.madness')).to.equal(42);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
933
src/test/database-test.js
Normal file
933
src/test/database-test.js
Normal file
@@ -0,0 +1,933 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var appdb = require('../appdb.js'),
|
||||
authcodedb = require('../authcodedb.js'),
|
||||
clientdb = require('../clientdb.js'),
|
||||
hat = require('hat'),
|
||||
database = require('../database'),
|
||||
DatabaseError = require('../databaseerror.js'),
|
||||
expect = require('expect.js'),
|
||||
async = require('async'),
|
||||
settingsdb = require('../settingsdb.js'),
|
||||
tokendb = require('../tokendb.js'),
|
||||
userdb = require('../userdb.js');
|
||||
|
||||
describe('database', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear
|
||||
], done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
database._clear(done);
|
||||
});
|
||||
|
||||
describe('userdb', function () {
|
||||
var USER_0 = {
|
||||
id: 'uuid213',
|
||||
username: 'uuid213',
|
||||
password: 'secret',
|
||||
email: 'safe@me.com',
|
||||
admin: false,
|
||||
salt: 'morton',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256)
|
||||
};
|
||||
|
||||
var ADMIN_0 = {
|
||||
id: 'uuid456',
|
||||
username: 'uuid456',
|
||||
password: 'secret',
|
||||
email: 'safe2@me.com',
|
||||
admin: true,
|
||||
salt: 'tata',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: ''
|
||||
};
|
||||
|
||||
it('can add user', function (done) {
|
||||
userdb.add(USER_0.id, USER_0, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can add admin user', function (done) {
|
||||
userdb.add(ADMIN_0.id, ADMIN_0, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot add same user again', function (done) {
|
||||
userdb.add(USER_0.id, USER_0, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get by user id', function (done) {
|
||||
userdb.get(USER_0.id, function (error, user) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(user).to.eql(USER_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get by user name', function (done) {
|
||||
userdb.getByUsername(USER_0.username, function (error, user) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(user).to.eql(USER_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get by email', function (done) {
|
||||
userdb.getByEmail(USER_0.email, function (error, user) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(user).to.eql(USER_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get by resetToken fails for empty resetToken', function (done) {
|
||||
userdb.getByResetToken('', function (error, user) {
|
||||
expect(error).to.be.ok();
|
||||
expect(error.reason).to.be(DatabaseError.INTERNAL_ERROR);
|
||||
expect(user).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get by resetToken', function (done) {
|
||||
userdb.getByResetToken(USER_0.resetToken, function (error, user) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(user).to.eql(USER_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get all', function (done) {
|
||||
userdb.getAll(function (error, all) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(all.length).to.equal(2);
|
||||
expect(all[0]).to.eql(USER_0);
|
||||
expect(all[1]).to.eql(ADMIN_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get all admins', function (done) {
|
||||
userdb.getAllAdmins(function (error, all) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(all.length).to.equal(1);
|
||||
expect(all[0]).to.eql(ADMIN_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('counts the users', function (done) {
|
||||
userdb.count(function (error, count) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(count).to.equal(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('counts the admin users', function (done) {
|
||||
userdb.adminCount(function (error, count) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(count).to.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can update the user', function (done) {
|
||||
userdb.update(USER_0.id, { email: 'some@thing.com' }, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
userdb.get(USER_0.id, function (error, user) {
|
||||
expect(user.email).to.equal('some@thing.com');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot update with null field', function (done) {
|
||||
userdb.update(USER_0.id, { email: null }, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot del non-existing user', function (done) {
|
||||
userdb.del(USER_0.id + USER_0.id, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can del existing user', function (done) {
|
||||
userdb.del(USER_0.id, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('did remove the user', function (done) {
|
||||
userdb.count(function (error, count) {
|
||||
expect(count).to.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can clear table', function (done) {
|
||||
userdb._clear(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
userdb.count(function (error, count) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(count).to.equal(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('authcode', function () {
|
||||
var AUTHCODE_0 = {
|
||||
authCode: 'authcode-0',
|
||||
clientId: 'clientid-0',
|
||||
userId: 'userid-0',
|
||||
expiresAt: Date.now() + 5000
|
||||
};
|
||||
var AUTHCODE_1 = {
|
||||
authCode: 'authcode-1',
|
||||
clientId: 'clientid-1',
|
||||
userId: 'userid-1',
|
||||
expiresAt: Date.now() + 5000
|
||||
};
|
||||
var AUTHCODE_2 = {
|
||||
authCode: 'authcode-2',
|
||||
clientId: 'clientid-2',
|
||||
userId: 'userid-2',
|
||||
expiresAt: Date.now()
|
||||
};
|
||||
|
||||
it('add fails due to missing arguments', function () {
|
||||
expect(function () { authcodedb.add(AUTHCODE_0.authCode, AUTHCODE_0.clientId, AUTHCODE_0.userId); }).to.throwError();
|
||||
expect(function () { authcodedb.add(AUTHCODE_0.authCode, AUTHCODE_0.clientId, function () {}); }).to.throwError();
|
||||
expect(function () { authcodedb.add(AUTHCODE_0.authCode, function () {}); }).to.throwError();
|
||||
});
|
||||
|
||||
it('add succeeds', function (done) {
|
||||
authcodedb.add(AUTHCODE_0.authCode, AUTHCODE_0.clientId, AUTHCODE_0.userId, AUTHCODE_0.expiresAt, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('add of same authcode fails', function (done) {
|
||||
authcodedb.add(AUTHCODE_0.authCode, AUTHCODE_0.clientId, AUTHCODE_0.userId, AUTHCODE_0.expiresAt, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
authcodedb.get(AUTHCODE_0.authCode, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an('object');
|
||||
expect(result).to.be.eql(AUTHCODE_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get of nonexisting code fails', function (done) {
|
||||
authcodedb.get(AUTHCODE_1.authCode, function (error, result) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
expect(result).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get of expired code fails', function (done) {
|
||||
authcodedb.add(AUTHCODE_2.authCode, AUTHCODE_2.clientId, AUTHCODE_2.userId, AUTHCODE_2.expiresAt, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
authcodedb.get(AUTHCODE_2.authCode, function (error, result) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
expect(result).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('delExpired succeeds', function (done) {
|
||||
authcodedb.delExpired(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.eql(1);
|
||||
|
||||
authcodedb.get(AUTHCODE_2.authCode, function (error, result) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
expect(result).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('delete succeeds', function (done) {
|
||||
authcodedb.del(AUTHCODE_0.authCode, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot delete previously delete record', function (done) {
|
||||
authcodedb.del(AUTHCODE_0.authCode, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('token', function () {
|
||||
var TOKEN_0 = {
|
||||
accessToken: tokendb.generateToken(),
|
||||
identifier: tokendb.PREFIX_USER + '0',
|
||||
clientId: 'clientid-0',
|
||||
expires: Date.now() + 60 * 60000,
|
||||
scope: '*'
|
||||
};
|
||||
var TOKEN_1 = {
|
||||
accessToken: tokendb.generateToken(),
|
||||
identifier: tokendb.PREFIX_USER + '1',
|
||||
clientId: 'clientid-1',
|
||||
expires: Number.MAX_SAFE_INTEGER,
|
||||
scope: '*'
|
||||
};
|
||||
var TOKEN_2 = {
|
||||
accessToken: tokendb.generateToken(),
|
||||
identifier: tokendb.PREFIX_USER + '2',
|
||||
clientId: 'clientid-2',
|
||||
expires: Date.now(),
|
||||
scope: '*'
|
||||
};
|
||||
|
||||
it('add fails due to missing arguments', function () {
|
||||
expect(function () { tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.scope); }).to.throwError();
|
||||
expect(function () { tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, function () {}); }).to.throwError();
|
||||
expect(function () { tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, function () {}); }).to.throwError();
|
||||
expect(function () { tokendb.add(TOKEN_0.accessToken, function () {}); }).to.throwError();
|
||||
});
|
||||
|
||||
it('add succeeds', function (done) {
|
||||
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('add of same token fails', function (done) {
|
||||
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
tokendb.get(TOKEN_0.accessToken, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an('object');
|
||||
expect(result).to.be.eql(TOKEN_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get of nonexisting token fails', function (done) {
|
||||
tokendb.get(TOKEN_1.accessToken, function (error, result) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
expect(result).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getByIdentifier succeeds', function (done) {
|
||||
tokendb.getByIdentifier(TOKEN_0.identifier, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(Array);
|
||||
expect(result.length).to.equal(1);
|
||||
expect(result[0]).to.be.an('object');
|
||||
expect(result[0]).to.be.eql(TOKEN_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('delete succeeds', function (done) {
|
||||
tokendb.del(TOKEN_0.accessToken, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getByIdentifier succeeds after token deletion', function (done) {
|
||||
tokendb.getByIdentifier(TOKEN_0.identifier, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(Array);
|
||||
expect(result.length).to.equal(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('delByIdentifier succeeds', function (done) {
|
||||
tokendb.add(TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
tokendb.delByIdentifier(TOKEN_1.identifier, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot delete previously delete record', function (done) {
|
||||
tokendb.del(TOKEN_0.accessToken, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getByIdentifierAndClientId succeeds', function (done) {
|
||||
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
tokendb.getByIdentifierAndClientId(TOKEN_0.identifier, TOKEN_0.clientId, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(Array);
|
||||
expect(result.length).to.equal(1);
|
||||
expect(result[0]).to.eql(TOKEN_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('delExpired succeeds', function (done) {
|
||||
tokendb.add(TOKEN_2.accessToken, TOKEN_2.identifier, TOKEN_2.clientId, TOKEN_2.expires, TOKEN_2.scope, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
tokendb.delExpired(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.eql(1);
|
||||
|
||||
tokendb.get(TOKEN_2.accessToken, function (error, result) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
expect(result).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('delByIdentifierAndClientId succeeds', function (done) {
|
||||
tokendb.delByIdentifierAndClientId(TOKEN_0.identifier, TOKEN_0.clientId, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get of previously deleted token fails', function (done) {
|
||||
tokendb.get(TOKEN_0.accessToken, function (error, result) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
expect(result).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('app', function () {
|
||||
var APP_0 = {
|
||||
id: 'appid-0',
|
||||
appStoreId: 'appStoreId-0',
|
||||
dnsRecordId: null,
|
||||
installationState: appdb.ISTATE_PENDING_INSTALL,
|
||||
installationProgress: null,
|
||||
runState: null,
|
||||
location: 'some-location-0',
|
||||
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
|
||||
httpPort: null,
|
||||
containerId: null,
|
||||
portBindings: { port: 5678 },
|
||||
health: null,
|
||||
accessRestriction: '',
|
||||
lastBackupId: null,
|
||||
lastBackupConfig: null,
|
||||
oldConfig: null
|
||||
};
|
||||
var APP_1 = {
|
||||
id: 'appid-1',
|
||||
appStoreId: 'appStoreId-1',
|
||||
dnsRecordId: null,
|
||||
installationState: appdb.ISTATE_PENDING_INSTALL, // app health tests rely on this initial state
|
||||
installationProgress: null,
|
||||
runState: null,
|
||||
location: 'some-location-1',
|
||||
manifest: { version: '0.2', dockerImage: 'docker/app1', healthCheckPath: '/', httpPort: 80, title: 'app1' },
|
||||
httpPort: null,
|
||||
containerId: null,
|
||||
portBindings: { },
|
||||
health: null,
|
||||
accessRestriction: 'roleAdmin',
|
||||
lastBackupId: null,
|
||||
lastBackupConfig: null,
|
||||
oldConfig: null
|
||||
};
|
||||
|
||||
it('add fails due to missing arguments', function () {
|
||||
expect(function () { appdb.add(APP_0.id, APP_0.manifest, APP_0.installationState, function () {}); }).to.throwError();
|
||||
expect(function () { appdb.add(APP_0.id, function () {}); }).to.throwError();
|
||||
});
|
||||
|
||||
it('exists returns false', function (done) {
|
||||
appdb.exists(APP_0.id, function (error, exists) {
|
||||
expect(error).to.be(null);
|
||||
expect(exists).to.be(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('add succeeds', function (done) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('exists succeeds', function (done) {
|
||||
appdb.exists(APP_0.id, function (error, exists) {
|
||||
expect(error).to.be(null);
|
||||
expect(exists).to.be(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getPortBindings succeeds', function (done) {
|
||||
appdb.getPortBindings(APP_0.id, function (error, bindings) {
|
||||
expect(error).to.be(null);
|
||||
expect(bindings).to.be.an(Object);
|
||||
expect(bindings).to.be.eql({ port: '5678' });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('add of same app fails', function (done) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
appdb.get(APP_0.id, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an('object');
|
||||
expect(result).to.be.eql(APP_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get of nonexisting code fails', function (done) {
|
||||
appdb.get(APP_1.id, function (error, result) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
expect(result).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('update succeeds', function (done) {
|
||||
APP_0.installationState = 'some-other-status';
|
||||
APP_0.location = 'some-other-location';
|
||||
APP_0.manifest.version = '0.2';
|
||||
APP_0.accessRestriction = true;
|
||||
APP_0.httpPort = 1337;
|
||||
|
||||
appdb.update(APP_0.id, { installationState: APP_0.installationState, location: APP_0.location, manifest: APP_0.manifest, accessRestriction: APP_0.accessRestriction, httpPort: APP_0.httpPort }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
appdb.get(APP_0.id, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an('object');
|
||||
expect(result).to.be.eql(APP_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('getByHttpPort succeeds', function (done) {
|
||||
appdb.getByHttpPort(APP_0.httpPort, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an('object');
|
||||
expect(result).to.be.eql(APP_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('update of nonexisting app fails', function (done) {
|
||||
appdb.update(APP_1.id, { installationState: APP_1.installationState, location: APP_1.location }, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('add second app succeeds', function (done) {
|
||||
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getAll succeeds', function (done) {
|
||||
appdb.getAll(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(Array);
|
||||
expect(result.length).to.be(2);
|
||||
expect(result[0]).to.be.eql(APP_0);
|
||||
expect(result[1]).to.be.eql(APP_1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getAppStoreIds succeeds', function (done) {
|
||||
appdb.getAppStoreIds(function (error, results) {
|
||||
expect(error).to.be(null);
|
||||
expect(results).to.be.an(Array);
|
||||
expect(results.length).to.be(2);
|
||||
expect(results[0].appStoreId).to.equal(APP_0.appStoreId);
|
||||
expect(results[1].appStoreId).to.equal(APP_1.appStoreId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('delete succeeds', function (done) {
|
||||
appdb.del(APP_0.id, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getPortBindings should be empty', function (done) {
|
||||
appdb.getPortBindings(APP_0.id, function (error, bindings) {
|
||||
expect(error).to.be(null);
|
||||
expect(bindings).to.be.an(Object);
|
||||
expect(bindings).to.be.eql({ });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot delete previously delete record', function (done) {
|
||||
appdb.del(APP_0.id, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set app as healthy because app is not installed', function (done) {
|
||||
appdb.setHealth(APP_1.id, appdb.HEALTH_HEALTHY, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set app as healthy because app has pending run state', function (done) {
|
||||
appdb.update(APP_1.id, { runState: appdb.RSTATE_PENDING_STOP, installationState: appdb.ISTATE_INSTALLED }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
appdb.setHealth(APP_1.id, appdb.HEALTH_HEALTHY, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set app as healthy because app has null run state', function (done) {
|
||||
appdb.update(APP_1.id, { runState: null, installationState: appdb.ISTATE_INSTALLED }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
appdb.setHealth(APP_1.id, appdb.HEALTH_HEALTHY, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can set app as healthy when installed and no pending runState', function (done) {
|
||||
appdb.update(APP_1.id, { runState: appdb.RSTATE_RUNNING, installationState: appdb.ISTATE_INSTALLED }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
appdb.setHealth(APP_1.id, appdb.HEALTH_HEALTHY, function (error) {
|
||||
expect(error).to.be(null);
|
||||
appdb.get(APP_1.id, function (error, app) {
|
||||
expect(error).to.be(null);
|
||||
expect(app.health).to.be(appdb.HEALTH_HEALTHY);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set health of unknown app', function (done) {
|
||||
appdb.setHealth('randomId', appdb.HEALTH_HEALTHY, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('return empty addon config array for invalid app', function (done) {
|
||||
appdb.getAddonConfigByAppId('randomid', function (error, results) {
|
||||
expect(error).to.be(null);
|
||||
expect(results).to.eql([ ]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('setAddonConfig succeeds', function (done) {
|
||||
appdb.setAddonConfig(APP_1.id, 'addonid1', [ 'ENV1=env', 'ENV2=env' ], function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('setAddonConfig succeeds', function (done) {
|
||||
appdb.setAddonConfig(APP_1.id, 'addonid2', [ 'ENV3=env' ], function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getAddonConfig succeeds', function (done) {
|
||||
appdb.getAddonConfig(APP_1.id, 'addonid1', function (error, results) {
|
||||
expect(error).to.be(null);
|
||||
expect(results).to.eql([ 'ENV1=env', 'ENV2=env' ]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getAddonConfigByAppId succeeds', function (done) {
|
||||
appdb.getAddonConfigByAppId(APP_1.id, function (error, results) {
|
||||
expect(error).to.be(null);
|
||||
expect(results).to.eql([ 'ENV1=env', 'ENV2=env', 'ENV3=env' ]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('unsetAddonConfig succeeds', function (done) {
|
||||
appdb.unsetAddonConfig(APP_1.id, 'addonid1', function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('unsetAddonConfig did remove configs', function (done) {
|
||||
appdb.getAddonConfigByAppId(APP_1.id, function (error, results) {
|
||||
expect(error).to.be(null);
|
||||
expect(results).to.eql([ 'ENV3=env' ]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('unsetAddonConfigByAppId succeeds', function (done) {
|
||||
appdb.unsetAddonConfigByAppId(APP_1.id, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('unsetAddonConfigByAppId did remove configs', function (done) {
|
||||
appdb.getAddonConfigByAppId(APP_1.id, function (error, results) {
|
||||
expect(error).to.be(null);
|
||||
expect(results).to.eql([ ]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('client', function () {
|
||||
var CLIENT_0 = {
|
||||
id: 'cid-0',
|
||||
appId: 'someappid_0',
|
||||
clientSecret: 'secret-0',
|
||||
redirectURI: 'http://foo.bar',
|
||||
scope: '*'
|
||||
|
||||
};
|
||||
var CLIENT_1 = {
|
||||
id: 'cid-1',
|
||||
appId: 'someappid_1',
|
||||
clientSecret: 'secret-',
|
||||
redirectURI: 'http://foo.bar',
|
||||
scope: '*'
|
||||
};
|
||||
var CLIENT_2 = {
|
||||
id: 'cid-2',
|
||||
appId: 'someappid_2',
|
||||
clientSecret: 'secret-2',
|
||||
redirectURI: 'http://foo.bar.baz',
|
||||
scope: 'profile,roleUser'
|
||||
};
|
||||
|
||||
it('add succeeds', function (done) {
|
||||
clientdb.add(CLIENT_0.id, CLIENT_0.appId, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
clientdb.add(CLIENT_1.id, CLIENT_1.appId, CLIENT_1.clientSecret, CLIENT_1.redirectURI, CLIENT_1.scope, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('add same client id fails', function (done) {
|
||||
clientdb.add(CLIENT_0.id, CLIENT_0.appId, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.equal(DatabaseError.ALREADY_EXISTS);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
clientdb.get(CLIENT_0.id, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.eql(CLIENT_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getByAppId succeeds', function (done) {
|
||||
clientdb.getByAppId(CLIENT_0.appId, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.eql(CLIENT_0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getByAppId fails for unknown client id', function (done) {
|
||||
clientdb.getByAppId(CLIENT_0.appId + CLIENT_0.appId, function (error, result) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.equal(DatabaseError.NOT_FOUND);
|
||||
expect(result).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getAll succeeds', function (done) {
|
||||
clientdb.getAll(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(Array);
|
||||
expect(result.length).to.equal(3); // one of them is webadmin
|
||||
expect(result[0]).to.eql(CLIENT_0);
|
||||
expect(result[1]).to.eql(CLIENT_1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('update client fails due to unknown client id', function (done) {
|
||||
clientdb.update(CLIENT_2.id, CLIENT_2.appId, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.equal(DatabaseError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('update client succeeds', function (done) {
|
||||
clientdb.update(CLIENT_1.id, CLIENT_2.appId, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
clientdb.get(CLIENT_1.id, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.appId).to.eql(CLIENT_2.appId);
|
||||
expect(result.clientSecret).to.eql(CLIENT_2.clientSecret);
|
||||
expect(result.redirectURI).to.eql(CLIENT_2.redirectURI);
|
||||
expect(result.scope).to.eql(CLIENT_2.scope);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('delByAppId succeeds', function (done) {
|
||||
clientdb.delByAppId(CLIENT_0.appId, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
clientdb.getByAppId(CLIENT_0.appId, function (error, result) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.equal(DatabaseError.NOT_FOUND);
|
||||
expect(result).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('settings', function () {
|
||||
it('can set value', function (done) {
|
||||
settingsdb.set('somekey', 'somevalue', function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can get the set value', function (done) {
|
||||
settingsdb.get('somekey', function (error, value) {
|
||||
expect(error).to.be(null);
|
||||
expect(value).to.be('somevalue');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can get all values', function (done) {
|
||||
settingsdb.getAll(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(Array);
|
||||
expect(result[0].name).to.be('somekey');
|
||||
expect(result[0].value).to.be('somevalue');
|
||||
expect(result.length).to.be(1); // the value set above
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can update a value', function (done) {
|
||||
settingsdb.set('somekey', 'someothervalue', function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can get updated value', function (done) {
|
||||
settingsdb.get('somekey', function (error, value) {
|
||||
expect(error).to.be(null);
|
||||
expect(value).to.be('someothervalue');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
273
src/test/server-test.js
Normal file
273
src/test/server-test.js
Normal file
@@ -0,0 +1,273 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var progress = require('../progress.js'),
|
||||
config = require('../config.js'),
|
||||
database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
nock = require('nock'),
|
||||
request = require('superagent'),
|
||||
server = require('../server.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
function cleanup(done) {
|
||||
done();
|
||||
}
|
||||
|
||||
describe('Server', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
before(function () {
|
||||
config.set('version', '0.5.0');
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
describe('startup', function () {
|
||||
it('start fails due to wrong arguments', function (done) {
|
||||
expect(function () { server.start(); }).to.throwException();
|
||||
expect(function () { server.start('foobar', function () {}); }).to.throwException();
|
||||
expect(function () { server.start(1337, function () {}); }).to.throwException();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
server.start(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('is reachable', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail because already running', function (done) {
|
||||
expect(server.start).to.throwException(function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
server.stop(function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('runtime', function () {
|
||||
before(function (done) {
|
||||
server.start(done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
server.stop(function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('random bad requests', function (done) {
|
||||
request.get(SERVER_URL + '/random', function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('version', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.version).to.equal('0.5.0');
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('status route is GET', function (done) {
|
||||
request.post(SERVER_URL + '/api/v1/cloudron/status')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/status')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('config', function () {
|
||||
before(function (done) {
|
||||
server.start(done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
server.stop(function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('config fails due missing token', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/config', function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('config fails due wrong token', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/config').query({ access_token: 'somewrongtoken' }).end(function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('progress', function () {
|
||||
before(function (done) {
|
||||
server.start(done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
server.stop(function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with no progress', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.update).to.be(null);
|
||||
expect(result.body.backup).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with update progress', function (done) {
|
||||
progress.set(progress.UPDATE, 13, 'This is some status string');
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.update).to.be.an('object');
|
||||
expect(result.body.update.percent).to.be.a('number');
|
||||
expect(result.body.update.percent).to.equal(13);
|
||||
expect(result.body.update.message).to.be.a('string');
|
||||
expect(result.body.update.message).to.equal('This is some status string');
|
||||
|
||||
expect(result.body.backup).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with no progress after clearing the update', function (done) {
|
||||
progress.clear(progress.UPDATE);
|
||||
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.update).to.be(null);
|
||||
expect(result.body.backup).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shutdown', function () {
|
||||
before(function (done) {
|
||||
server.start(done);
|
||||
});
|
||||
|
||||
it('fails due to wrong arguments', function (done) {
|
||||
expect(function () { server.stop(); }).to.throwException();
|
||||
expect(function () { server.stop('foobar'); }).to.throwException();
|
||||
expect(function () { server.stop(1337); }).to.throwException();
|
||||
expect(function () { server.stop({}); }).to.throwException();
|
||||
expect(function () { server.stop({ httpServer: {} }); }).to.throwException();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
server.stop(function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('is not reachable anymore', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/cloudron/status', function (error, result) {
|
||||
expect(error).to.not.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cors', function () {
|
||||
before(function (done) {
|
||||
server.start(function (error) {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
it('responds to OPTIONS', function (done) {
|
||||
request('OPTIONS', SERVER_URL + '/api/v1/cloudron/status')
|
||||
.set('Access-Control-Request-Method', 'GET')
|
||||
.set('Access-Control-Request-Headers', 'accept, origin, x-requested-with')
|
||||
.set('Origin', 'http://localhost')
|
||||
.end(function (res) {
|
||||
expect(res.headers['access-control-allow-methods']).to.be('GET, PUT, DELETE, POST, OPTIONS');
|
||||
expect(res.headers['access-control-allow-credentials']).to.be('true');
|
||||
expect(res.headers['access-control-allow-headers']).to.be('accept, origin, x-requested-with'); // mirrored from request
|
||||
expect(res.headers['access-control-allow-origin']).to.be('http://localhost'); // mirrors from request
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
server.stop(function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('heartbeat', function () {
|
||||
var successfulHeartbeatGet;
|
||||
|
||||
before(function (done) {
|
||||
server.start(done);
|
||||
|
||||
var scope = nock(config.apiServerOrigin());
|
||||
successfulHeartbeatGet = scope.get('/api/v1/boxes/' + config.fqdn() + '/heartbeat');
|
||||
successfulHeartbeatGet.reply(200);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
server.stop(done);
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('sends heartbeat', function (done) {
|
||||
setTimeout(function () {
|
||||
expect(successfulHeartbeatGet.counter).to.equal(1);
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
66
src/test/settings-test.js
Normal file
66
src/test/settings-test.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
settings = require('../settings.js');
|
||||
|
||||
function setup(done) {
|
||||
// ensure data/config/mount paths
|
||||
database.initialize(function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(done);
|
||||
}
|
||||
|
||||
describe('Settings', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('can get default timezone', function (done) {
|
||||
settings.getTimeZone(function (error, tz) {
|
||||
expect(error).to.be(null);
|
||||
expect(tz.length).to.not.be(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can get default autoupdate_pattern', function (done) {
|
||||
settings.getAutoupdatePattern(function (error, pattern) {
|
||||
expect(error).to.be(null);
|
||||
expect(pattern).to.be('00 00 1,3,5,23 * * *');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it ('can get default cloudron name', function (done) {
|
||||
settings.getCloudronName(function (error, name) {
|
||||
expect(error).to.be(null);
|
||||
expect(name).to.be('Cloudron');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can get default cloudron avatar', function (done) {
|
||||
settings.getCloudronAvatar(function (error, gravatar) {
|
||||
expect(error).to.be(null);
|
||||
expect(gravatar).to.be.a(Buffer);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can get all values', function (done) {
|
||||
settings.getAll(function (error, allSettings) {
|
||||
expect(error).to.be(null);
|
||||
expect(allSettings[settings.TIME_ZONE_KEY]).to.be.a('string');
|
||||
expect(allSettings[settings.AUTOUPDATE_PATTERN_KEY]).to.be.a('string');
|
||||
expect(allSettings[settings.CLOUDRON_NAME_KEY]).to.be.a('string');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
20
src/test/setupTest
Executable file
20
src/test/setupTest
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
readonly ADMIN_LOCATION=admin
|
||||
readonly source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)"
|
||||
|
||||
! "${source_dir}/src/test/checkInstall" && exit 1
|
||||
|
||||
# create dir structure
|
||||
rm -rf $HOME/.cloudron_test
|
||||
mkdir -p $HOME/.cloudron_test
|
||||
cd $HOME/.cloudron_test
|
||||
mkdir -p data/appdata data/box/appicons data/mail data/nginx/cert data/nginx/applications data/collectd/collectd.conf.d data/addons configs
|
||||
|
||||
webadmin_scopes="root,profile,users,apps,settings,roleAdmin"
|
||||
webadmin_origin="https://${ADMIN_LOCATION}-localhost"
|
||||
mysql --user=root --password="" \
|
||||
-e "REPLACE INTO clients (id, appId, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"webadmin\", \"secret-webadmin\", \"${webadmin_origin}\", \"${webadmin_scopes}\")" boxtest
|
||||
|
||||
51
src/test/shell-test.js
Normal file
51
src/test/shell-test.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global after:false */
|
||||
/* global before:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var expect = require('expect.js'),
|
||||
path = require('path'),
|
||||
shell = require('../shell.js');
|
||||
|
||||
describe('shell', function () {
|
||||
it('can run valid program', function (done) {
|
||||
var cp = shell.exec('test', 'ls', [ '-l' ], function (error) {
|
||||
expect(cp).to.be.ok();
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on invalid program', function (done) {
|
||||
var cp = shell.exec('test', 'randomprogram', [ ], function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on failing program', function (done) {
|
||||
var cp = shell.exec('test', '/usr/bin/false', [ ], function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot sudo invalid program', function (done) {
|
||||
var cp = shell.sudo('test', [ 'randomprogram' ], function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can sudo valid program', function (done) {
|
||||
var RELOAD_NGINX_CMD = path.join(__dirname, '../src/scripts/reloadnginx.sh');
|
||||
var cp = shell.sudo('test', [ RELOAD_NGINX_CMD ], function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
401
src/test/user-test.js
Normal file
401
src/test/user-test.js
Normal file
@@ -0,0 +1,401 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
user = require('../user.js'),
|
||||
userdb = require('../userdb.js'),
|
||||
UserError = user.UserError;
|
||||
|
||||
var USERNAME = 'nobody';
|
||||
var USERNAME_NEW = 'nobodynew';
|
||||
var EMAIL = 'nobody@no.body';
|
||||
var EMAIL_NEW = 'nobodynew@no.body';
|
||||
var PASSWORD = 'foobar';
|
||||
var NEW_PASSWORD = 'somenewpassword';
|
||||
var IS_ADMIN = true;
|
||||
|
||||
function cleanupUsers(done) {
|
||||
userdb._clear(function () {
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function createUser(done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, IS_ADMIN, null /* invitor */, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function setup(done) {
|
||||
// ensure data/config/mount paths
|
||||
database.initialize(function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(done);
|
||||
}
|
||||
|
||||
describe('User', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
describe('create', function() {
|
||||
before(cleanupUsers);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, IS_ADMIN, null /* invitor */, function (error, result) {
|
||||
expect(error).not.to.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.username).to.equal(USERNAME);
|
||||
expect(result.email).to.equal(EMAIL);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails because of invalid BAD_FIELD', function (done) {
|
||||
expect(function () {
|
||||
user.create(EMAIL, {}, function () {});
|
||||
}).to.throwException();
|
||||
expect(function () {
|
||||
user.create(12345, PASSWORD, EMAIL, function () {});
|
||||
}).to.throwException();
|
||||
expect(function () {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, {});
|
||||
}).to.throwException();
|
||||
expect(function () {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, {}, function () {});
|
||||
}).to.throwException();
|
||||
expect(function () {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, {});
|
||||
}).to.throwException();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('fails because user exists', function (done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, IS_ADMIN, null /* invitor */, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).not.to.be.ok();
|
||||
expect(error.reason).to.equal(UserError.ALREADY_EXISTS);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails because password is empty', function (done) {
|
||||
user.create(USERNAME, '', EMAIL, IS_ADMIN, null /* invitor */, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).not.to.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('verify', function () {
|
||||
before(createUser);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to non existing username', function (done) {
|
||||
user.verify(USERNAME+USERNAME, PASSWORD, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.NOT_FOUND);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty password', function (done) {
|
||||
user.verify(USERNAME, '', function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.WRONG_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong password', function (done) {
|
||||
user.verify(USERNAME, PASSWORD+PASSWORD, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.WRONG_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.verify(USERNAME, PASSWORD, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyWithEmail', function () {
|
||||
before(createUser);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to non existing user', function (done) {
|
||||
user.verifyWithEmail(EMAIL+EMAIL, PASSWORD, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.NOT_FOUND);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty password', function (done) {
|
||||
user.verifyWithEmail(EMAIL, '', function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.WRONG_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong password', function (done) {
|
||||
user.verifyWithEmail(EMAIL, PASSWORD+PASSWORD, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.WRONG_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.verifyWithEmail(EMAIL, PASSWORD, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('retrieving', function () {
|
||||
before(createUser);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to non existing user', function (done) {
|
||||
user.get('some non existing username', function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.get(USERNAME, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', function () {
|
||||
before(createUser);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to unknown userid', function (done) {
|
||||
user.update(USERNAME+USERNAME, USERNAME_NEW, EMAIL_NEW, function (error) {
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.NOT_FOUND);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to invalid username', function (done) {
|
||||
user.update(USERNAME, '', EMAIL_NEW, function (error) {
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.BAD_USERNAME);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to invalid email', function (done) {
|
||||
user.update(USERNAME, USERNAME_NEW, 'brokenemailaddress', function (error) {
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.BAD_EMAIL);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.update(USERNAME, USERNAME_NEW, EMAIL_NEW, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
user.get(USERNAME, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.email).to.equal(EMAIL_NEW);
|
||||
expect(result.username).to.equal(USERNAME_NEW);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin change', function () {
|
||||
before(createUser);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails to remove admin flag of only admin', function (done) {
|
||||
user.changeAdmin(USERNAME, false, function (error) {
|
||||
expect(error).to.be.an('object');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('make second user admin succeeds', function (done) {
|
||||
var user1 = {
|
||||
username: 'seconduser',
|
||||
password: 'foobar',
|
||||
email: 'some@thi.ng'
|
||||
};
|
||||
|
||||
user.create(user1.username, user1.password, user1.email, false, { username: USERNAME, email: EMAIL } /* invitor */, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
user.changeAdmin(user1.username, true, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds to remove admin flag of first user', function (done) {
|
||||
user.changeAdmin(USERNAME, false, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('password change', function () {
|
||||
before(createUser);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to wrong arumgent count', function () {
|
||||
expect(function () { user.changePassword(); }).to.throwError();
|
||||
expect(function () { user.changePassword(USERNAME); }).to.throwError();
|
||||
expect(function () { user.changePassword(USERNAME, PASSWORD, NEW_PASSWORD); }).to.throwError();
|
||||
});
|
||||
|
||||
it('fails due to wrong arumgents', function () {
|
||||
expect(function () { user.changePassword(USERNAME, {}, NEW_PASSWORD, function () {}); }).to.throwError();
|
||||
expect(function () { user.changePassword(1337, PASSWORD, NEW_PASSWORD, function () {}); }).to.throwError();
|
||||
expect(function () { user.changePassword(USERNAME, PASSWORD, 1337, function () {}); }).to.throwError();
|
||||
expect(function () { user.changePassword(USERNAME, PASSWORD, NEW_PASSWORD, 'some string'); }).to.throwError();
|
||||
});
|
||||
|
||||
it('fails due to wrong password', function (done) {
|
||||
user.changePassword(USERNAME, 'wrongpassword', NEW_PASSWORD, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty new password', function (done) {
|
||||
user.changePassword(USERNAME, PASSWORD, '', function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to unknown user', function (done) {
|
||||
user.changePassword('somerandomuser', PASSWORD, NEW_PASSWORD, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.changePassword(USERNAME, PASSWORD, NEW_PASSWORD, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('actually changed the password (unable to login with old pasword)', function (done) {
|
||||
user.verify(USERNAME, PASSWORD, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.WRONG_PASSWORD);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('actually changed the password (login with new password)', function (done) {
|
||||
user.verify(USERNAME, NEW_PASSWORD, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPasswordByIdentifier', function () {
|
||||
before(createUser);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to unkown email', function (done) {
|
||||
user.resetPasswordByIdentifier('unknown@mail.com', function (error) {
|
||||
expect(error).to.be.an(UserError);
|
||||
expect(error.reason).to.eql(UserError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to unkown username', function (done) {
|
||||
user.resetPasswordByIdentifier('unknown', function (error) {
|
||||
expect(error).to.be.an(UserError);
|
||||
expect(error.reason).to.eql(UserError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with email', function (done) {
|
||||
user.resetPasswordByIdentifier(EMAIL, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with username', function (done) {
|
||||
user.resetPasswordByIdentifier(USERNAME, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
147
src/tokendb.js
Normal file
147
src/tokendb.js
Normal file
@@ -0,0 +1,147 @@
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
generateToken: generateToken,
|
||||
get: get,
|
||||
add: add,
|
||||
del: del,
|
||||
getByIdentifier: getByIdentifier,
|
||||
delByIdentifier: delByIdentifier,
|
||||
getByIdentifierAndClientId: getByIdentifierAndClientId,
|
||||
delByIdentifierAndClientId: delByIdentifierAndClientId,
|
||||
delExpired: delExpired,
|
||||
|
||||
TYPE_USER: 'user',
|
||||
TYPE_DEV: 'developer',
|
||||
TYPE_APP: 'appliation',
|
||||
|
||||
PREFIX_USER: 'user-',
|
||||
PREFIX_DEV: 'dev-',
|
||||
PREFIX_APP: 'app-',
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
hat = require('hat');
|
||||
|
||||
|
||||
var TOKENS_FIELDS = [ 'accessToken', 'identifier', 'clientId', 'scope', 'expires' ].join(',');
|
||||
|
||||
function generateToken() {
|
||||
return hat(256);
|
||||
}
|
||||
|
||||
function get(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + TOKENS_FIELDS + ' FROM tokens WHERE accessToken = ? AND expires > ?', [ accessToken, Date.now() ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function add(accessToken, identifier, clientId, expires, scope, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert(typeof clientId === 'string' || clientId === null);
|
||||
assert.strictEqual(typeof expires, 'number');
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO tokens (accessToken, identifier, clientId, expires, scope) VALUES (?, ?, ?, ?, ?)',
|
||||
[ accessToken, identifier, clientId, expires, scope ], function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM tokens WHERE accessToken = ?', [ accessToken ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function getByIdentifier(identifier, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + TOKENS_FIELDS + ' FROM tokens WHERE identifier = ?', [ identifier ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function delByIdentifier(identifier, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM tokens WHERE identifier = ?', [ identifier ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getByIdentifierAndClientId(identifier, clientId, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + TOKENS_FIELDS + ' FROM tokens WHERE identifier=? AND clientId=?', [ identifier, clientId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function delByIdentifierAndClientId(identifier, clientId, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM tokens WHERE identifier = ? AND clientId = ?', [ identifier, clientId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function delExpired(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM tokens WHERE expires <= ?', [ Date.now() ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
return callback(null, result.affectedRows);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM tokens', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
160
src/updatechecker.js
Normal file
160
src/updatechecker.js
Normal file
@@ -0,0 +1,160 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
checkAppUpdates: checkAppUpdates,
|
||||
checkBoxUpdates: checkBoxUpdates,
|
||||
|
||||
getUpdateInfo: getUpdateInfo
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:updatechecker'),
|
||||
fs = require('fs'),
|
||||
mailer = require('./mailer.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { console.error(error); };
|
||||
|
||||
var gAppUpdateInfo = { }, // id -> update info { creationDate, manifest }
|
||||
gBoxUpdateInfo = null,
|
||||
gMailedUser = { };
|
||||
|
||||
function getUpdateInfo() {
|
||||
return {
|
||||
apps: gAppUpdateInfo,
|
||||
box: gBoxUpdateInfo
|
||||
};
|
||||
}
|
||||
|
||||
function getAppUpdates(callback) {
|
||||
apps.getAll(function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var appUpdateInfo = { };
|
||||
// appStoreId can be '' for dev apps
|
||||
var appStoreIds = apps.map(function (app) { return app.appStoreId; }).filter(function (id) { return id !== ''; });
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/appupdates')
|
||||
.send({ appIds: appStoreIds, boxVersion: config.version() })
|
||||
.timeout(10 * 1000)
|
||||
.end(function (error, result) {
|
||||
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.statusCode !== 200 || !result.body.appVersions) {
|
||||
return callback(new Error(util.format('Error checking app update: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
|
||||
var latestAppVersions = result.body.appVersions;
|
||||
for (var i = 0; i < apps.length; i++) {
|
||||
if (!(apps[i].appStoreId in latestAppVersions)) continue;
|
||||
|
||||
var oldVersion = apps[i].manifest.version;
|
||||
|
||||
var newManifest = latestAppVersions[apps[i].appStoreId].manifest;
|
||||
var newVersion = newManifest.version;
|
||||
if (newVersion !== oldVersion) {
|
||||
appUpdateInfo[apps[i].id] = latestAppVersions[apps[i].appStoreId];
|
||||
debug('Update available for %s (%s) from %s to %s', apps[i].location, apps[i].id, oldVersion, newVersion);
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, appUpdateInfo);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getBoxUpdates(callback) {
|
||||
var currentVersion = config.version();
|
||||
|
||||
superagent
|
||||
.get(config.get('boxVersionsUrl'))
|
||||
.timeout(10 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.status !== 200) return callback(new Error(util.format('Bad status: %s %s', result.status, result.text)));
|
||||
|
||||
var versions = safe.JSON.parse(result.text);
|
||||
|
||||
if (!versions || typeof versions !== 'object') return callback(new Error('versions is not in valid format:' + safe.error));
|
||||
|
||||
var latestVersion = Object.keys(versions).sort(semver.compare).pop();
|
||||
debug('checkBoxUpdates: Latest version is %s etag:%s', latestVersion, result.header['etag']);
|
||||
|
||||
if (!latestVersion) return callback(new Error('No version available'));
|
||||
|
||||
var nextVersion = null, nextVersionInfo = null;
|
||||
var currentVersionInfo = versions[currentVersion];
|
||||
if (!currentVersionInfo) {
|
||||
debug('Cloudron runs on unknown version %s. Offering to update to latest version', currentVersion);
|
||||
nextVersion = latestVersion;
|
||||
nextVersionInfo = versions[latestVersion];
|
||||
} else {
|
||||
nextVersion = currentVersionInfo.next;
|
||||
nextVersionInfo = nextVersion ? versions[nextVersion] : null;
|
||||
}
|
||||
|
||||
if (nextVersionInfo && typeof nextVersionInfo === 'object') {
|
||||
debug('new version %s available. imageId: %d code: %s', nextVersion, nextVersionInfo.imageId, nextVersionInfo.sourceTarballUrl);
|
||||
callback(null, {
|
||||
version: nextVersion,
|
||||
changelog: nextVersionInfo.changelog,
|
||||
upgrade: nextVersionInfo.upgrade
|
||||
});
|
||||
} else {
|
||||
debug('no new version available.');
|
||||
callback(null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkAppUpdates() {
|
||||
debug('Checking App Updates');
|
||||
|
||||
getAppUpdates(function (error, result) {
|
||||
if (error) debug('Error checking app updates: ', error);
|
||||
|
||||
gAppUpdateInfo = error ? {} : result;
|
||||
|
||||
async.eachSeries(Object.keys(gAppUpdateInfo), function iterator(id, iteratorDone) {
|
||||
if (gMailedUser[id]) return iteratorDone();
|
||||
|
||||
apps.get(id, function (error, app) {
|
||||
if (error) {
|
||||
debug('Error getting app %s %s', id, error);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
mailer.appUpdateAvailable(app, gAppUpdateInfo[id]);
|
||||
gMailedUser[id] = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkBoxUpdates() {
|
||||
debug('Checking Box Updates');
|
||||
|
||||
getBoxUpdates(function (error, result) {
|
||||
if (error) debug('Error checking box updates: ', error);
|
||||
|
||||
gBoxUpdateInfo = error ? null : result;
|
||||
|
||||
if (gBoxUpdateInfo && !gMailedUser['box']) {
|
||||
mailer.boxUpdateAvailable(gBoxUpdateInfo.version, gBoxUpdateInfo.changelog);
|
||||
gMailedUser['box'] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
384
src/user.js
Normal file
384
src/user.js
Normal file
@@ -0,0 +1,384 @@
|
||||
/* jshint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
UserError: UserError,
|
||||
|
||||
list: listUsers,
|
||||
create: createUser,
|
||||
verify: verify,
|
||||
verifyWithEmail: verifyWithEmail,
|
||||
remove: removeUser,
|
||||
get: getUser,
|
||||
getByResetToken: getByResetToken,
|
||||
changeAdmin: changeAdmin,
|
||||
resetPasswordByIdentifier: resetPasswordByIdentifier,
|
||||
setPassword: setPassword,
|
||||
changePassword: changePassword,
|
||||
update: updateUser,
|
||||
createOwner: createOwner
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
crypto = require('crypto'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
hat = require('hat'),
|
||||
userdb = require('./userdb.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
util = require('util'),
|
||||
validator = require('validator'),
|
||||
_ = require('underscore');
|
||||
|
||||
var CRYPTO_SALT_SIZE = 64; // 512-bit salt
|
||||
var CRYPTO_ITERATIONS = 10000; // iterations
|
||||
var CRYPTO_KEY_LENGTH = 512; // bits
|
||||
|
||||
// http://dustinsenos.com/articles/customErrorsInNode
|
||||
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
|
||||
function UserError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(UserError, Error);
|
||||
UserError.INTERNAL_ERROR = 'Internal Error';
|
||||
UserError.ALREADY_EXISTS = 'Already Exists';
|
||||
UserError.NOT_FOUND = 'Not Found';
|
||||
UserError.WRONG_PASSWORD = 'Wrong User or Password';
|
||||
UserError.BAD_FIELD = 'Bad field';
|
||||
UserError.BAD_USERNAME = 'Bad username';
|
||||
UserError.BAD_EMAIL = 'Bad email';
|
||||
UserError.BAD_PASSWORD = 'Bad password';
|
||||
UserError.BAD_TOKEN = 'Bad token';
|
||||
UserError.NOT_ALLOWED = 'Not Allowed';
|
||||
|
||||
function listUsers(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.getAll(function (error, result) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result.map(function (obj) { return _.pick(obj, 'id', 'username', 'email', 'admin'); }));
|
||||
});
|
||||
}
|
||||
|
||||
function validateUsername(username) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
|
||||
if (username.length <= 2) return new UserError(UserError.BAD_USERNAME, 'Username must be atleast 3 chars');
|
||||
if (username.length > 256) return new UserError(UserError.BAD_USERNAME, 'Username too long');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validatePassword(password) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
if (password.length < 5) return new UserError(UserError.BAD_PASSWORD, 'Password must be atleast 5 chars');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateEmail(email) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
|
||||
if (!validator.isEmail(email)) return new UserError(UserError.BAD_EMAIL, 'Invalid email');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateToken(token) {
|
||||
assert.strictEqual(typeof token, 'string');
|
||||
|
||||
if (token.length !== 64) return new UserError(UserError.BAD_TOKEN, 'Invalid token'); // 256-bit hex coded token
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function createUser(username, password, email, admin, invitor, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof admin, 'boolean');
|
||||
assert(invitor || admin);
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateUsername(username);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePassword(password);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateEmail(email);
|
||||
if (error) return callback(error);
|
||||
|
||||
crypto.randomBytes(CRYPTO_SALT_SIZE, function (error, salt) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
crypto.pbkdf2(password, salt, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
var now = (new Date()).toUTCString();
|
||||
var user = {
|
||||
id: username,
|
||||
username: username,
|
||||
email: email,
|
||||
password: new Buffer(derivedKey, 'binary').toString('hex'),
|
||||
admin: admin,
|
||||
salt: salt.toString('hex'),
|
||||
createdAt: now,
|
||||
modifiedAt: now,
|
||||
resetToken: hat(256)
|
||||
};
|
||||
|
||||
userdb.add(user.id, user, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, user);
|
||||
|
||||
// only send welcome mail if user is not an admin. This is only the case for the first user!
|
||||
// The welcome email contains a link to create a new password
|
||||
if (!user.admin) mailer.userAdded(user, invitor);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verify(username, password, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.get(username, function (error, user) {
|
||||
if (error && error.reason == DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
var saltBinary = new Buffer(user.salt, 'hex');
|
||||
crypto.pbkdf2(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
var derivedKeyHex = new Buffer(derivedKey, 'binary').toString('hex');
|
||||
if (derivedKeyHex !== user.password) return callback(new UserError(UserError.WRONG_PASSWORD));
|
||||
|
||||
callback(null, user);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verifyWithEmail(email, password, callback) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.getByEmail(email, function (error, user) {
|
||||
if (error && error.reason == DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
var saltBinary = new Buffer(user.salt, 'hex');
|
||||
crypto.pbkdf2(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
var derivedKeyHex = new Buffer(derivedKey, 'binary').toString('hex');
|
||||
if (derivedKeyHex !== user.password) return callback(new UserError(UserError.WRONG_PASSWORD));
|
||||
|
||||
callback(null, user);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeUser(username, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.del(username, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
|
||||
mailer.userRemoved(username);
|
||||
});
|
||||
}
|
||||
|
||||
function getUser(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.get(userId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getByResetToken(resetToken, callback) {
|
||||
assert.strictEqual(typeof resetToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateToken(resetToken);
|
||||
if (error) return callback(error);
|
||||
|
||||
userdb.getByResetToken(resetToken, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser(userId, username, email, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateUsername(username);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateEmail(email);
|
||||
if (error) return callback(error);
|
||||
|
||||
userdb.update(userId, { username: username, email: email }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, error));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function changeAdmin(username, admin, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof admin, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getUser(username, function (error, user) {
|
||||
if (error) return callback(error);
|
||||
|
||||
userdb.getAllAdmins(function (error, result) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
// protect from a system where there is no admin left
|
||||
if (result.length <= 1 && !admin) return callback(new UserError(UserError.NOT_ALLOWED, 'Only admin'));
|
||||
|
||||
user.admin = admin;
|
||||
|
||||
userdb.update(username, user, function (error) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
|
||||
mailer.adminChanged(user);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetPasswordByIdentifier(identifier, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var getter;
|
||||
if (identifier.indexOf('@') === -1) getter = userdb.getByUsername;
|
||||
else getter = userdb.getByEmail;
|
||||
|
||||
getter(identifier, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
result.resetToken = hat(256);
|
||||
|
||||
userdb.update(result.id, result, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
mailer.passwordReset(result);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setPassword(userId, newPassword, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof newPassword, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validatePassword(newPassword);
|
||||
if (error) return callback(error);
|
||||
|
||||
userdb.get(userId, function (error, user) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
var saltBuffer = new Buffer(user.salt, 'hex');
|
||||
crypto.pbkdf2(newPassword, saltBuffer, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
user.modifiedAt = (new Date()).toUTCString();
|
||||
user.password = new Buffer(derivedKey, 'binary').toString('hex');
|
||||
user.resetToken = '';
|
||||
|
||||
userdb.update(userId, user, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
// Also generate a token so the new user can get logged in immediately
|
||||
clientdb.getByAppId('webadmin', function (error, result) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var expiresAt = Date.now() + 24 * 60 * 60 * 1000; // 1 day
|
||||
|
||||
tokendb.add(token, tokendb.PREFIX_USER + user.id, result.id, expiresAt, '*', function (error) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, { token: token, expiresAt: expiresAt });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function changePassword(username, oldPassword, newPassword, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof oldPassword, 'string');
|
||||
assert.strictEqual(typeof newPassword, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validatePassword(newPassword);
|
||||
if (error) return callback(error);
|
||||
|
||||
verify(username, oldPassword, function (error, user) {
|
||||
if (error) return callback(error);
|
||||
|
||||
setPassword(user.id, newPassword, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function createOwner(username, password, email, callback) {
|
||||
userdb.count(function (error, count) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
if (count !== 0) return callback(new UserError(UserError.ALREADY_EXISTS));
|
||||
|
||||
createUser(username, password, email, true /* admin */, null /* invitor */, callback);
|
||||
});
|
||||
}
|
||||
|
||||
189
src/userdb.js
Normal file
189
src/userdb.js
Normal file
@@ -0,0 +1,189 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getByUsername: getByUsername,
|
||||
getByEmail: getByEmail,
|
||||
getByAccessToken: getByAccessToken,
|
||||
getByResetToken: getByResetToken,
|
||||
getAll: getAll,
|
||||
getAllAdmins: getAllAdmins,
|
||||
add: add,
|
||||
del: del,
|
||||
update: update,
|
||||
count: count,
|
||||
adminCount: adminCount,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
debug = require('debug')('box:userdb'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
|
||||
var USERS_FIELDS = [ 'id', 'username', 'email', 'password', 'salt', 'createdAt', 'modifiedAt', 'admin', 'resetToken' ].join(',');
|
||||
|
||||
function get(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE id = ?', [ userId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getByUsername(username, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// currently username is also our id
|
||||
get(username, callback);
|
||||
}
|
||||
|
||||
function getByEmail(email, callback) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE email = ?', [ email ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getByResetToken(resetToken, callback) {
|
||||
assert.strictEqual(typeof resetToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (resetToken.length === 0) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'Empty resetToken not allowed'));
|
||||
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE resetToken=?', [ resetToken ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function getAllAdmins(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE admin=1', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function add(userId, user, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof user.username, 'string');
|
||||
assert.strictEqual(typeof user.password, 'string');
|
||||
assert.strictEqual(typeof user.email, 'string');
|
||||
assert.strictEqual(typeof user.admin, 'boolean');
|
||||
assert.strictEqual(typeof user.salt, 'string');
|
||||
assert.strictEqual(typeof user.createdAt, 'string');
|
||||
assert.strictEqual(typeof user.modifiedAt, 'string');
|
||||
assert.strictEqual(typeof user.resetToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [ userId, user.username, user.password, user.email, user.admin, user.salt, user.createdAt, user.modifiedAt, user.resetToken ];
|
||||
database.query('INSERT INTO users (id, username, password, email, admin, salt, createdAt, modifiedAt, resetToken) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
data, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM users WHERE id = ?', [ userId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function getByAccessToken(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getByAccessToken: ' + accessToken);
|
||||
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users, tokens WHERE tokens.accessToken = ?', [ accessToken ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
database.query('DELETE FROM users', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function update(userId, user, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var args = [ ];
|
||||
var fields = [ ];
|
||||
for (var k in user) {
|
||||
fields.push(k + ' = ?');
|
||||
args.push(user[k]);
|
||||
}
|
||||
args.push(userId);
|
||||
|
||||
database.query('UPDATE users SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function count(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT COUNT(*) AS total FROM users', function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result[0].total);
|
||||
});
|
||||
}
|
||||
|
||||
function adminCount(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT COUNT(*) AS total FROM users WHERE admin=1', function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result[0].total);
|
||||
});
|
||||
}
|
||||
|
||||
40
src/vbox.js
Normal file
40
src/vbox.js
Normal file
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
// we can possibly remove this entire file and make our tests
|
||||
// smarter to just use the host interface provided by boot2docker
|
||||
// https://github.com/boot2docker/boot2docker#container-port-redirection
|
||||
// https://github.com/boot2docker/boot2docker/pull/93
|
||||
// https://github.com/docker/docker/issues/4007
|
||||
|
||||
exports = module.exports = {
|
||||
forwardFromHostToVirtualBox: forwardFromHostToVirtualBox,
|
||||
unforwardFromHostToVirtualBox: unforwardFromHostToVirtualBox
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
child_process = require('child_process'),
|
||||
debug = require('debug')('box:vbox'),
|
||||
os = require('os');
|
||||
|
||||
|
||||
function forwardFromHostToVirtualBox(rulename, port) {
|
||||
assert.strictEqual(typeof rulename, 'string');
|
||||
assert.strictEqual(typeof port, 'number');
|
||||
|
||||
if (os.platform() === 'darwin') {
|
||||
debug('Setting up VirtualBox port forwarding for '+ rulename + ' at ' + port);
|
||||
child_process.exec(
|
||||
'VBoxManage controlvm boot2docker-vm natpf1 delete ' + rulename + ';' +
|
||||
'VBoxManage controlvm boot2docker-vm natpf1 ' + rulename + ',tcp,127.0.0.1,' + port + ',,' + port);
|
||||
}
|
||||
}
|
||||
|
||||
function unforwardFromHostToVirtualBox(rulename) {
|
||||
assert.strictEqual(typeof rulename, 'string');
|
||||
|
||||
if (os.platform() === 'darwin') {
|
||||
debug('Removing VirtualBox port forwarding for '+ rulename);
|
||||
child_process.exec('VBoxManage controlvm boot2docker-vm natpf1 delete ' + rulename);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user