app.portBindings and newManifest.tcpPorts may be null

This commit is contained in:
Girish Ramakrishnan
2015-07-20 00:09:47 -07:00
commit df9d321ac3
243 changed files with 42623 additions and 0 deletions

356
src/routes/apps.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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();
});
});
});
});

View 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();
});
});
});
});
});
});

View 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();
});
});
});
});

View 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();
});
});
});
});

View 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();
});
});
});
});

View 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
View 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 ""

View 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
View 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();
}