Files
cloudron-box/src/routes/apps.js

767 lines
30 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
getApp: getApp,
getApps: getApps,
getAppIcon: getAppIcon,
install: install,
uninstall: uninstall,
restore: restore,
importApp: importApp,
backup: backup,
update: update,
getLogs: getLogs,
getLogStream: getLogStream,
2016-01-19 13:35:28 +01:00
listBackups: listBackups,
repair: repair,
2019-09-08 16:57:08 -07:00
setAccessRestriction: setAccessRestriction,
setLabel: setLabel,
setTags: setTags,
setIcon: setIcon,
setMemoryLimit: setMemoryLimit,
2020-01-28 21:30:35 -08:00
setCpuShares: setCpuShares,
2019-09-08 16:57:08 -07:00
setAutomaticBackup: setAutomaticBackup,
setAutomaticUpdate: setAutomaticUpdate,
setReverseProxyConfig: setReverseProxyConfig,
2019-09-08 16:57:08 -07:00
setCertificate: setCertificate,
setDebugMode: setDebugMode,
setEnvironment: setEnvironment,
setMailbox: setMailbox,
setLocation: setLocation,
setDataDir: setDataDir,
stop: stop,
start: start,
restart: restart,
2016-06-17 17:12:55 -05:00
exec: exec,
execWebSocket: execWebSocket,
2016-06-17 17:12:55 -05:00
clone: clone,
uploadFile: uploadFile,
downloadFile: downloadFile,
load: load
};
var apps = require('../apps.js'),
assert = require('assert'),
2019-03-25 15:07:06 -07:00
auditSource = require('../auditsource.js'),
2019-10-24 10:39:47 -07:00
BoxError = require('../boxerror.js'),
debug = require('debug')('box:routes/apps'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
users = require('../users.js'),
util = require('util'),
WebSocket = require('ws');
function load(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.get(req.params.id, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
req.resource = result;
next();
});
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
next(new HttpSuccess(200, apps.removeInternalFields(req.resource)));
}
function getApps(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
apps.getAllByUser(req.user, function (error, allApps) {
if (error) return next(BoxError.toHttpError(error));
allApps = allApps.map(apps.removeRestrictedFields);
next(new HttpSuccess(200, { apps: allApps }));
});
}
function getAppIcon(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
apps.getIconPath(req.resource, { original: req.query.original }, function (error, iconPath) {
if (error) return next(BoxError.toHttpError(error));
2019-05-17 09:47:11 -07:00
res.sendFile(iconPath);
});
}
function install(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
const data = req.body;
// atleast one
2016-06-04 19:19:00 -07:00
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
2016-06-03 23:22:38 -07:00
// required
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
2017-11-02 22:17:44 +01:00
if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required'));
if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
2016-06-03 23:22:38 -07:00
// optional
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'));
2019-03-22 07:48:31 -07:00
if ('label' in data && typeof data.label !== 'string') return next(new HttpError(400, 'label must be a string'));
// falsy values in cert and key unset the cert
if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (data.cert && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
2015-10-28 22:09:19 +01:00
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
2016-02-11 17:00:21 +01:00
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
if ('alternateDomains' in data) {
if (!Array.isArray(data.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
}
if ('env' in data) {
if (!data.env || typeof data.env !== 'object') return next(new HttpError(400, 'env must be an object'));
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
}
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return next(BoxError.toHttpError(error));
2020-03-29 20:12:59 -07:00
if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to install app with docker addon'));
data.appStoreId = appStoreId;
data.manifest = manifest;
apps.install(data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
});
});
}
2019-09-08 16:57:08 -07:00
function setAccessRestriction(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
apps.setAccessRestriction(req.resource, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(200, {}));
});
}
function setLabel(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (typeof req.body.label !== 'string') return next(new HttpError(400, 'label must be a string'));
apps.setLabel(req.resource, req.body.label, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(200, {}));
});
}
function setTags(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (!Array.isArray(req.body.tags)) return next(new HttpError(400, 'tags must be an array'));
if (req.body.tags.some((t) => typeof t !== 'string')) return next(new HttpError(400, 'tags array must contain strings'));
apps.setTags(req.resource, req.body.tags, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(200, {}));
});
}
function setIcon(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (req.body.icon !== null && typeof req.body.icon !== 'string') return next(new HttpError(400, 'icon is null or a base-64 image string'));
apps.setIcon(req.resource, req.body.icon, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(200, {}));
});
}
function setMemoryLimit(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
apps.setMemoryLimit(req.resource, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2020-01-28 21:30:35 -08:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function setCpuShares(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2020-01-28 21:30:35 -08:00
if (typeof req.body.cpuShares !== 'number') return next(new HttpError(400, 'cpuShares is not a number'));
apps.setCpuShares(req.resource, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) {
2020-01-28 21:30:35 -08:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function setAutomaticBackup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
apps.setAutomaticBackup(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(200, {}));
});
}
function setAutomaticUpdate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
apps.setAutomaticUpdate(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(200, {}));
});
}
function setReverseProxyConfig(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (req.body.robotsTxt !== null && typeof req.body.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt is not a string'));
if (req.body.csp !== null && typeof req.body.csp !== 'string') return next(new HttpError(400, 'csp is not a string'));
apps.setReverseProxyConfig(req.resource, req.body, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(200, {}));
});
}
function setCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (req.body.key !== null && typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (req.body.cert !== null && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (req.body.cert && !req.body.key) return next(new HttpError(400, 'key must be provided'));
if (!req.body.cert && req.body.key) return next(new HttpError(400, 'cert must be provided'));
apps.setCertificate(req.resource, req.body, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(200, {}));
});
}
function setEnvironment(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (!req.body.env || typeof req.body.env !== 'object') return next(new HttpError(400, 'env must be an object'));
2019-09-09 15:35:02 -07:00
if (Object.keys(req.body.env).some((key) => typeof req.body.env[key] !== 'string')) return next(new HttpError(400, 'env must contain values as strings'));
2019-09-08 16:57:08 -07:00
apps.setEnvironment(req.resource, req.body.env, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function setDebugMode(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (req.body.debugMode !== null && typeof req.body.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
apps.setDebugMode(req.resource, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function setMailbox(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (req.body.mailboxName !== null && typeof req.body.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
2019-11-14 21:43:14 -08:00
if (typeof req.body.mailboxDomain !== 'string') return next(new HttpError(400, 'mailboxDomain must be a string'));
2019-09-08 16:57:08 -07:00
apps.setMailbox(req.resource, req.body.mailboxName, req.body.mailboxDomain, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function setLocation(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
if (typeof req.body.location !== 'string') return next(new HttpError(400, 'location must be string')); // location may be an empty string
2019-09-08 16:57:08 -07:00
if (!req.body.domain) return next(new HttpError(400, 'domain is required'));
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
if ('portBindings' in req.body && typeof req.body.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('alternateDomains' in req.body) {
if (!Array.isArray(req.body.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
if (req.body.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
}
2019-09-10 15:23:47 -07:00
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.setLocation(req.resource, req.body, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function setDataDir(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2019-09-08 16:57:08 -07:00
2019-09-09 16:37:59 -07:00
if (req.body.dataDir !== null && typeof req.body.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
2019-09-08 16:57:08 -07:00
apps.setDataDir(req.resource, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function repair(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
const data = req.body;
Fix repair If a task fails, we can either: * allow other task ops to be called - we cannot do this because the ops are fine-grained. for example, a restore failure removes many things and calling set-memory or set-location in that state won't make sense. * provide a generic repair route - this allows one to override args and call the failed task again. this is what we have now but has the issue that this repair function has to know about all the other op functions. for example, for argument validation. we can do some complicated refactoring to make it work if we want. * just a generic total re-configure - this does not work because clone/restore/backup/datadir/uninstall/update failure leaves the app in a state which re-configure cannot do anything about. * allow the failed op to be called again - this seems the easiest. we just allow the route to be called again in the error state. * if we hit a state where even providing extra args, cannot get you out of this "error" state, we have to provide some repair route. for example, maybe the container disappeared by some docke error. user clicks 'repair' to recreate the container. this route does not have to take any args. The final solution is: * a failed task can be called again via the route. so we can resubmit any args and we get validation * repair route just re-configures and can be called in any state to just rebuild container. re-configure is also doing only local changes (docker, nginx) * install/clone failures are fixed using repair route. updated manifest can be passed in. * UI shows backup selector for restore failures * UI shows domain selector for change location failulre
2019-11-23 18:35:51 -08:00
if ('manifest' in data) {
if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
2020-03-29 20:12:59 -07:00
if (safe.query(data.manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to repair app with docker addon'));
}
if ('dockerImage' in data) {
if (!data.dockerImage || typeof data.dockerImage !== 'string') return next(new HttpError(400, 'dockerImage must be a string'));
}
apps.repair(req.resource, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function restore(req, res, next) {
2016-06-13 10:08:58 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2016-06-13 10:08:58 -07:00
var data = req.body;
2019-12-05 21:15:09 -08:00
if (!data.backupId || typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be non-empty string'));
apps.restore(req.resource, data.backupId, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2019-08-27 20:55:49 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function importApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
var data = req.body;
if ('backupId' in data) { // if not provided, we import in-place
if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string'));
if (typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string'));
if ('backupConfig' in data && typeof data.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig must be an object'));
const backupConfig = req.body.backupConfig;
if (req.body.backupConfig) {
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
// testing backup config can take sometime
req.clearTimeout();
}
}
apps.importApp(req.resource, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function clone(req, res, next) {
2016-06-17 17:12:55 -05:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
2016-06-17 17:12:55 -05:00
var data = req.body;
if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string'));
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
2017-11-02 22:17:44 +01:00
if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required'));
2016-06-17 17:12:55 -05:00
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.clone(req.resource, data, req.user, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2016-06-17 17:12:55 -05:00
2019-08-27 20:55:49 -07:00
next(new HttpSuccess(201, { id: result.id, taskId: result.taskId }));
2016-06-17 17:12:55 -05:00
});
}
function backup(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
apps.backup(req.resource, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2019-08-27 20:55:49 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function uninstall(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
apps.uninstall(req.resource, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2019-08-27 20:55:49 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function start(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
apps.start(req.resource, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function stop(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
apps.stop(req.resource, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function restart(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
2019-12-20 10:29:29 -08:00
apps.restart(req.resource, auditSource.fromRequest(req), function (error, result) {
2019-12-20 10:29:29 -08:00
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
var data = req.body;
2016-06-04 19:19:00 -07:00
// atleast one
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
if ('skipBackup' in data && typeof data.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return next(BoxError.toHttpError(error));
2020-03-29 20:12:59 -07:00
if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to update app with docker addon'));
data.appStoreId = appStoreId;
data.manifest = manifest;
2020-03-30 15:05:37 -07:00
apps.update(req.resource, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
});
}
// this route is for streaming logs
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
2015-11-02 11:20:50 -08:00
if (isNaN(lines)) return next(new HttpError(400, 'lines 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'));
2017-04-19 21:43:29 -07:00
var options = {
lines: lines,
follow: true,
format: 'json'
2017-04-19 21:43:29 -07:00
};
apps.getLogs(req.resource, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(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.realtimeTimestamp, JSON.stringify(obj))); // send timestamp as id
});
logStream.on('end', res.end.bind(res));
logStream.on('error', res.end.bind(res, null));
});
}
function getLogs(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10;
2015-11-02 11:20:50 -08:00
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
var options = {
lines: lines,
follow: false,
format: req.query.format || 'json'
};
apps.getLogs(req.resource, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(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 demuxStream(stream, stdin) {
var header = null;
stream.on('readable', function() {
header = header || stream.read(4);
while (header !== null) {
var length = header.readUInt32BE(0);
if (length === 0) {
header = null;
return stdin.end(); // EOF
}
var payload = stream.read(length);
if (payload === null) break;
stdin.write(payload);
header = stream.read(4);
}
});
}
function exec(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
var cmd = null;
if (req.query.cmd) {
cmd = safe.JSON.parse(req.query.cmd);
2016-05-19 15:50:17 -07:00
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'));
var tty = req.query.tty === 'true';
2020-03-29 20:12:59 -07:00
if (safe.query(req.resource, 'manifest.addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is requied to exec app with docker addon'));
2016-01-18 11:16:06 -08:00
// in a badly configured reverse proxy, we might be here without an upgrade
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(BoxError.toHttpError(error));
req.clearTimeout();
res.sendUpgradeHandshake();
// When tty is disabled, the duplexStream has 2 separate streams. When enabled, it has stdout/stderr merged.
duplexStream.pipe(res.socket);
if (tty) {
res.socket.pipe(duplexStream); // in tty mode, the client always waits for server to exit
} else {
demuxStream(res.socket, duplexStream);
res.socket.on('error', function () { duplexStream.end(); });
res.socket.on('end', function () { duplexStream.end(); });
}
});
}
2016-01-19 13:35:28 +01:00
function execWebSocket(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
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'));
var tty = req.query.tty === 'true' ? true : false;
// in a badly configured reverse proxy, we might be here without an upgrade
if (req.headers['upgrade'] !== 'websocket') return next(new HttpError(404, 'exec requires websocket'));
apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(BoxError.toHttpError(error));
req.clearTimeout();
res.handleUpgrade(function (ws) {
duplexStream.on('end', function () { ws.close(); });
duplexStream.on('close', function () { ws.close(); });
duplexStream.on('error', function (error) {
2018-11-11 21:57:45 -08:00
debug('duplexStream error:', error);
});
duplexStream.on('data', function (data) {
if (ws.readyState !== WebSocket.OPEN) return;
ws.send(data.toString());
});
ws.on('error', function (error) {
2018-11-11 21:57:45 -08:00
debug('websocket error:', error);
});
ws.on('message', function (msg) {
duplexStream.write(msg);
});
ws.on('close', function () {
// Clean things up, if any?
});
});
});
}
2016-01-19 13:35:28 +01:00
function listBackups(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
2016-01-19 13:35:28 +01:00
2016-03-08 08:57:28 -08:00
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
apps.listBackups(req.resource, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2016-01-19 13:35:28 +01:00
next(new HttpSuccess(200, { backups: result }));
});
}
function uploadFile(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart'));
apps.uploadFile(req.resource, req.files.file.path, req.query.file, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
function downloadFile(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
apps.downloadFile(req.resource, req.query.file, function (error, stream, info) {
if (error) return next(BoxError.toHttpError(error));
2017-08-20 18:44:26 -07:00
var headers = {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename*=utf-8''${encodeURIComponent(info.filename)}` // RFC 2184 section 4
2017-08-20 18:44:26 -07:00
};
if (info.size) headers['Content-Length'] = info.size;
res.writeHead(200, headers);
2017-08-20 18:44:26 -07:00
stream.pipe(res);
});
}