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

782 lines
32 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
2020-10-27 17:11:50 -07:00
getApp,
2021-08-20 09:19:44 -07:00
listByUser,
2020-10-27 17:11:50 -07:00
getAppIcon,
install,
uninstall,
restore,
importApp,
exportApp,
2020-10-27 17:11:50 -07:00
backup,
update,
getLogs,
getLogStream,
listBackups,
repair,
2020-10-27 17:11:50 -07:00
setAccessRestriction,
setLabel,
setTags,
setIcon,
setMemoryLimit,
setCpuShares,
setAutomaticBackup,
setAutomaticUpdate,
setReverseProxyConfig,
setCertificate,
setDebugMode,
setEnvironment,
setMailbox,
setLocation,
setDataDir,
2020-10-28 19:42:48 -07:00
setMounts,
2019-09-08 16:57:08 -07:00
2020-10-27 17:11:50 -07:00
stop,
start,
restart,
exec,
execWebSocket,
2016-06-17 17:12:55 -05:00
2020-10-27 17:11:50 -07:00
clone,
2020-10-27 17:11:50 -07:00
uploadFile,
downloadFile,
2020-10-27 17:11:50 -07:00
load
};
2021-08-30 09:27:35 -07:00
const 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'),
WebSocket = require('ws');
2021-08-20 09:19:44 -07:00
async function load(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
2021-08-23 15:44:23 -07:00
const [error, result] = await safe(apps.get(req.params.id));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2021-08-30 09:27:35 -07:00
if (!result) return next(new HttpError(404, 'App not found'));
req.app = result;
2021-08-20 09:19:44 -07:00
next();
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.app, 'object');
next(new HttpSuccess(200, apps.removeInternalFields(req.app)));
}
2021-08-20 09:19:44 -07:00
async function listByUser(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
2021-08-20 09:19:44 -07:00
let [error, result] = await safe(apps.listByUser(req.user));
if (error) return next(BoxError.toHttpError(error));
2021-08-20 09:19:44 -07:00
result = result.map(apps.removeRestrictedFields);
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(200, { apps: result }));
}
2021-08-20 09:19:44 -07:00
async function getAppIcon(req, res, next) {
assert.strictEqual(typeof req.app, 'object');
const [error, icon] = await safe(apps.getIcon(req.app, { original: req.query.original }));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-05-17 09:47:11 -07:00
2021-08-20 09:19:44 -07:00
res.send(icon);
}
2021-08-20 09:19:44 -07:00
async 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'));
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'));
}
2021-01-18 17:26:26 -08:00
if ('aliasDomains' in data) {
if (!Array.isArray(data.aliasDomains)) return next(new HttpError(400, 'aliasDomains must be an array'));
if (data.aliasDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'aliasDomains 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'));
if ('skipDnsSetup' in req.body && typeof req.body.skipDnsSetup !== 'boolean') return next(new HttpError(400, 'skipDnsSetup must be boolean'));
if ('enableMailbox' in req.body && typeof req.body.enableMailbox !== 'boolean') return next(new HttpError(400, 'enableMailbox must be boolean'));
2021-08-20 09:19:44 -07:00
let [error, result] = await safe(apps.downloadManifest(data.appStoreId, data.manifest));
if (error) return next(BoxError.toHttpError(error));
2021-08-20 09:19:44 -07:00
if (safe.query(result.manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to install app with docker addon'));
2021-08-20 09:19:44 -07:00
data.appStoreId = result.appStoreId;
data.manifest = result.manifest;
2021-08-20 09:19:44 -07:00
[error, result] = await safe(apps.install(data, auditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
}
2021-08-20 09:19:44 -07:00
async function setAccessRestriction(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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'));
const [error] = await safe(apps.setAccessRestriction(req.app, req.body.accessRestriction, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(200, {}));
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setLabel(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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'));
const [error] = await safe(apps.setLabel(req.app, req.body.label, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(200, {}));
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setTags(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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'));
const [error] = await safe(apps.setTags(req.app, req.body.tags, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(200, {}));
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setIcon(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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'));
const [error] = await safe(apps.setIcon(req.app, req.body.icon || null /* empty string means null */, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(200, {}));
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setMemoryLimit(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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'));
const [error, result] = await safe(apps.setMemoryLimit(req.app, req.body.memoryLimit, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2020-01-28 21:30:35 -08:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
2020-01-28 21:30:35 -08:00
}
function setCpuShares(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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.app, 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 }));
});
}
2021-08-20 09:19:44 -07:00
async function setAutomaticBackup(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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'));
const [error] = await safe(apps.setAutomaticBackup(req.app, req.body.enable, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(200, {}));
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setAutomaticUpdate(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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'));
const [error] = await safe(apps.setAutomaticUpdate(req.app, req.body.enable, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(200, {}));
2019-09-08 16:57:08 -07:00
}
2021-08-17 14:04:29 -07:00
async function setReverseProxyConfig(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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'));
const [error] = await safe(apps.setReverseProxyConfig(req.app, req.body, auditSource.fromRequest(req)));
2021-08-17 14:04:29 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-17 14:04:29 -07:00
next(new HttpSuccess(200, {}));
2019-09-08 16:57:08 -07:00
}
2021-08-17 14:04:29 -07:00
async function setCertificate(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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
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'));
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'));
const [error] = await safe(apps.setCertificate(req.app, req.body, auditSource.fromRequest(req)));
2021-08-17 14:04:29 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-17 14:04:29 -07:00
next(new HttpSuccess(200, {}));
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setEnvironment(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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
const [error, result] = await safe(apps.setEnvironment(req.app, req.body.env, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setDebugMode(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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'));
const [error, result] = await safe(apps.setDebugMode(req.app, req.body.debugMode, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setMailbox(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, 'object');
2019-09-08 16:57:08 -07:00
2021-03-16 22:38:59 -07:00
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
if (req.body.enable) {
if (req.body.mailboxName !== null && typeof req.body.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
if (typeof req.body.mailboxDomain !== 'string') return next(new HttpError(400, 'mailboxDomain must be a string'));
}
2019-09-08 16:57:08 -07:00
const [error, result] = await safe(apps.setMailbox(req.app, req.body, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setLocation(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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'));
}
2021-01-18 17:26:26 -08:00
if ('aliasDomains' in req.body) {
if (!Array.isArray(req.body.aliasDomains)) return next(new HttpError(400, 'aliasDomains must be an array'));
if (req.body.aliasDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'aliasDomains 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'));
if ('skipDnsSetup' in req.body && typeof req.body.skipDnsSetup !== 'boolean') return next(new HttpError(400, 'skipDnsSetup must be boolean'));
2019-09-10 15:23:47 -07:00
const [error, result] = await safe(apps.setLocation(req.app, req.body, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setDataDir(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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
const [error, result] = await safe(apps.setDataDir(req.app, req.body.dataDir, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function repair(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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'));
}
const [error, result] = await safe(apps.repair(req.app, data, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
}
2021-07-14 11:07:19 -07:00
async function restore(req, res, next) {
2016-06-13 10:08:58 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, 'object');
2021-07-14 11:07:19 -07:00
const data = req.body;
2016-06-13 10:08:58 -07:00
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'));
const [error, result] = await safe(apps.restore(req.app, data.backupId, auditSource.fromRequest(req)));
2021-07-14 11:07:19 -07:00
if (error) return next(BoxError.toHttpError(error));
2021-07-14 11:07:19 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
}
2021-08-20 09:19:44 -07:00
async function importApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, 'object');
2021-08-20 09:19:44 -07:00
const 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 ('password' in backupConfig && typeof backupConfig.password !== 'string') return next(new HttpError(400, 'password 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();
}
}
const [error, result] = await safe(apps.importApp(req.app, data, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
}
2021-08-20 09:19:44 -07:00
async function exportApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, 'object');
const [error, result] = await safe(apps.exportApp(req.app, {}, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
}
2021-08-20 09:19:44 -07:00
async function clone(req, res, next) {
2016-06-17 17:12:55 -05:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, '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'));
if ('skipDnsSetup' in req.body && typeof req.body.skipDnsSetup !== 'boolean') return next(new HttpError(400, 'skipDnsSetup must be boolean'));
const [error, result] = await safe(apps.clone(req.app, data, req.user, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2016-06-17 17:12:55 -05:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(201, { id: result.id, taskId: result.taskId }));
2016-06-17 17:12:55 -05:00
}
2021-08-20 09:19:44 -07:00
async function backup(req, res, next) {
assert.strictEqual(typeof req.app, 'object');
const [error, result] = await safe(apps.backup(req.app));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
}
2021-08-20 09:19:44 -07:00
async function uninstall(req, res, next) {
assert.strictEqual(typeof req.app, 'object');
const [error, result] = await safe(apps.uninstall(req.app, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
}
2021-08-20 09:19:44 -07:00
async function start(req, res, next) {
assert.strictEqual(typeof req.app, 'object');
const [error, result] = await safe(apps.start(req.app, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
}
2021-08-20 09:19:44 -07:00
async function stop(req, res, next) {
assert.strictEqual(typeof req.app, 'object');
const [error, result] = await safe(apps.stop(req.app, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
}
2021-08-20 09:19:44 -07:00
async function restart(req, res, next) {
assert.strictEqual(typeof req.app, 'object');
2019-12-20 10:29:29 -08:00
const [error, result] = await safe(apps.restart(req.app, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-12-20 10:29:29 -08:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
2019-12-20 10:29:29 -08:00
}
2021-08-20 09:19:44 -07:00
async function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, 'object');
2021-08-20 09:19:44 -07:00
const 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'));
2021-08-20 09:19:44 -07:00
let [error, result] = await safe(apps.downloadManifest(data.appStoreId, data.manifest));
if (error) return next(BoxError.toHttpError(error));
2021-08-20 09:19:44 -07:00
const { appStoreId, manifest } = result;
2021-08-20 09:19:44 -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'));
2021-08-20 09:19:44 -07:00
data.appStoreId = appStoreId;
data.manifest = manifest;
[error, result] = await safe(apps.updateApp(req.app, data, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
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.app, '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.app, 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.app, '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.app, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
'Content-Disposition': `attachment; filename="${req.app.id}.log"`,
'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);
}
});
}
2021-08-25 19:41:46 -07:00
async function exec(req, res, next) {
assert.strictEqual(typeof req.app, 'object');
2021-08-25 19:41:46 -07:00
let cmd = null;
if (req.query.cmd) {
cmd = safe.JSON.parse(req.query.cmd);
2021-05-02 11:26:08 -07:00
if (!Array.isArray(cmd) || cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1'));
}
2021-08-25 19:41:46 -07:00
const columns = req.query.columns ? parseInt(req.query.columns, 10) : null;
if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number'));
2021-08-25 19:41:46 -07:00
const rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
2021-08-25 19:41:46 -07:00
const tty = req.query.tty === 'true';
if (safe.query(req.app, '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'));
const [error, duplexStream] = await safe(apps.exec(req.app, { cmd: cmd, rows: rows, columns: columns, tty: tty }));
2021-08-25 19:41:46 -07:00
if (error) return next(BoxError.toHttpError(error));
2021-08-25 19:41:46 -07:00
req.clearTimeout();
res.sendUpgradeHandshake();
2021-08-25 19:41:46 -07:00
// When tty is disabled, the duplexStream has 2 separate streams. When enabled, it has stdout/stderr merged.
duplexStream.pipe(res.socket);
2021-08-25 19:41:46 -07:00
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
2021-08-25 19:41:46 -07:00
async function execWebSocket(req, res, next) {
assert.strictEqual(typeof req.app, 'object');
2021-08-25 19:41:46 -07:00
let cmd = null;
if (req.query.cmd) {
cmd = safe.JSON.parse(req.query.cmd);
2021-05-02 11:26:08 -07:00
if (!Array.isArray(cmd) || cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1'));
}
2021-08-25 19:41:46 -07:00
const columns = req.query.columns ? parseInt(req.query.columns, 10) : null;
if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number'));
2021-08-25 19:41:46 -07:00
const rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
2021-08-25 19:41:46 -07:00
const 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'));
const [error, duplexStream] = await safe(apps.exec(req.app, { cmd: cmd, rows: rows, columns: columns, tty: tty }));
2021-08-25 19:41:46 -07:00
if (error) return next(BoxError.toHttpError(error));
2021-08-25 19:41:46 -07:00
req.clearTimeout();
res.handleUpgrade(function (ws) {
duplexStream.on('end', function () { ws.close(); });
duplexStream.on('close', function () { ws.close(); });
duplexStream.on('error', function (error) {
debug('duplexStream error:', error);
});
duplexStream.on('data', function (data) {
if (ws.readyState !== WebSocket.OPEN) return;
ws.send(data.toString());
});
ws.on('error', function (error) {
debug('websocket error:', error);
});
ws.on('message', function (msg) {
duplexStream.write(msg);
});
ws.on('close', function () {
// Clean things up, if any?
});
});
}
2021-07-14 11:07:19 -07:00
async function listBackups(req, res, next) {
assert.strictEqual(typeof req.app, 'object');
2016-01-19 13:35:28 +01:00
2021-07-14 11:07:19 -07:00
const page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
2016-03-08 08:57:28 -08:00
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
2021-07-14 11:07:19 -07:00
const perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
2016-03-08 08:57:28 -08:00
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
const [error, result] = await safe(apps.listBackups(req.app, page, perPage));
2021-07-14 11:07:19 -07:00
if (error) return next(BoxError.toHttpError(error));
2016-01-19 13:35:28 +01:00
2021-07-14 11:07:19 -07:00
next(new HttpSuccess(200, { backups: result }));
2016-01-19 13:35:28 +01:00
}
function uploadFile(req, res, next) {
assert.strictEqual(typeof req.app, '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.app, 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.app, 'object');
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
apps.downloadFile(req.app, 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);
});
}
2020-04-29 21:55:21 -07:00
2021-08-20 09:19:44 -07:00
async function setMounts(req, res, next) {
2020-04-29 21:55:21 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.app, 'object');
2020-04-29 21:55:21 -07:00
2020-10-29 21:58:14 -07:00
if (!Array.isArray(req.body.mounts)) return next(new HttpError(400, 'mounts should be an array'));
2020-10-28 19:42:48 -07:00
for (let m of req.body.mounts) {
if (!m || typeof m !== 'object') return next(new HttpError(400, 'mounts must be an object'));
if (typeof m.volumeId !== 'string') return next(new HttpError(400, 'volumeId must be a string'));
if (typeof m.readOnly !== 'boolean') return next(new HttpError(400, 'readOnly must be a boolean'));
2020-04-29 21:55:21 -07:00
}
const [error, result] = await safe(apps.setMounts(req.app, req.body.mounts, auditSource.fromRequest(req)));
2021-08-20 09:19:44 -07:00
if (error) return next(BoxError.toHttpError(error));
2020-04-29 21:55:21 -07:00
2021-08-20 09:19:44 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
2020-04-29 21:55:21 -07:00
}