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

812 lines
32 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
2020-10-27 17:11:50 -07:00
getApp,
getApps,
getAppIcon,
install,
uninstall,
restore,
importApp,
2020-12-06 19:38:50 -08:00
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,
2017-08-18 20:45:52 -07:00
2020-10-27 17:11:50 -07:00
uploadFile,
downloadFile,
2020-03-29 17:11:10 -07:00
2020-12-22 17:19:26 -08:00
2020-10-27 17:11:50 -07:00
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'),
WebSocket = require('ws');
2020-03-29 17:11:10 -07:00
function load(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
2020-03-29 17:11:10 -07:00
apps.get(req.params.id, function (error, result) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2020-03-29 17:11:10 -07:00
req.resource = result;
next();
});
}
2020-03-29 17:11:10 -07:00
function getApp(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
next(new HttpSuccess(200, apps.removeInternalFields(req.resource)));
}
function getApps(req, res, next) {
2016-02-25 12:20:11 +01:00
assert.strictEqual(typeof req.user, 'object');
apps.getAllByUser(req.user, function (error, allApps) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2018-04-26 20:07:03 -07:00
2018-06-25 16:45:15 -07:00
allApps = allApps.map(apps.removeRestrictedFields);
next(new HttpSuccess(200, { apps: allApps }));
});
}
function getAppIcon(req, res, next) {
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof req.resource, 'object');
2021-04-30 13:18:15 -07:00
apps.getIcon(req.resource, { original: req.query.original }, function (error, icon) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-05-17 09:47:11 -07:00
2021-04-30 13:18:15 -07:00
res.send(icon);
2019-09-01 17:44:24 -07:00
});
}
2020-03-29 17:11:10 -07:00
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'));
2016-11-11 10:55:44 +05:30
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
2017-08-16 14:12:07 -07:00
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
2018-12-07 09:03:28 -08:00
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'));
2017-01-19 11:20:24 -08:00
2018-09-04 16:21:10 -07:00
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'));
}
2018-10-11 14:07:43 -07:00
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'));
}
2019-09-16 09:31:34 -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-16 09:31:34 -07:00
apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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'));
2020-03-29 17:11:10 -07:00
apps.setAccessRestriction(req.resource, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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'));
2020-03-29 17:11:10 -07:00
apps.setLabel(req.resource, req.body.label, auditSource.fromRequest(req), function (error) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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'));
2020-03-29 17:11:10 -07:00
apps.setTags(req.resource, req.body.tags, auditSource.fromRequest(req), function (error) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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'));
2021-04-30 13:18:15 -07:00
apps.setIcon(req.resource, req.body.icon || null /* empty string means null */, auditSource.fromRequest(req), function (error) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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'));
2020-03-29 17:11:10 -07:00
apps.setMemoryLimit(req.resource, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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'));
2020-03-29 17:11:10 -07:00
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');
2020-03-29 17:11:10 -07:00
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'));
2020-03-29 17:11:10 -07:00
apps.setAutomaticBackup(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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'));
2020-03-29 17:11:10 -07:00
apps.setAutomaticUpdate(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(200, {}));
});
}
2019-10-13 18:22:03 -07:00
function setReverseProxyConfig(req, res, next) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof req.body, 'object');
2020-03-29 17:11:10 -07:00
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'));
2019-10-14 16:59:22 -07:00
if (req.body.csp !== null && typeof req.body.csp !== 'string') return next(new HttpError(400, 'csp is not a string'));
2019-10-13 18:22:03 -07:00
2020-03-29 17:11:10 -07:00
apps.setReverseProxyConfig(req.resource, req.body, auditSource.fromRequest(req), function (error) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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
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'));
2020-03-29 17:11:10 -07:00
apps.setCertificate(req.resource, req.body, auditSource.fromRequest(req), function (error) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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
2020-03-29 17:11:10 -07:00
apps.setEnvironment(req.resource, req.body.env, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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'));
2020-03-29 17:11:10 -07:00
apps.setDebugMode(req.resource, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof req.resource, '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
2021-03-16 22:38:59 -07:00
apps.setMailbox(req.resource, req.body, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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'));
}
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
2020-03-29 17:11:10 -07:00
apps.setLocation(req.resource, req.body, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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
2020-03-29 17:11:10 -07:00
apps.setDataDir(req.resource, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
2020-03-29 17:11:10 -07:00
function repair(req, res, next) {
2019-09-19 17:04:11 -07:00
assert.strictEqual(typeof req.body, 'object');
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof req.resource, 'object');
2019-09-19 17:04:11 -07:00
2019-09-21 19:45:55 -07:00
const data = req.body;
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'));
2019-09-21 19:45:55 -07:00
}
2020-03-29 17:11:10 -07:00
apps.repair(req.resource, data, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-19 17:04:11 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
2020-03-29 17:11:10 -07:00
function restore(req, res, next) {
2016-06-13 10:08:58 -07:00
assert.strictEqual(typeof req.body, 'object');
2020-03-29 17:11:10 -07:00
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'));
2020-03-29 17:11:10 -07:00
apps.restore(req.resource, data.backupId, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
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');
2020-03-29 17:11:10 -07:00
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'));
2020-05-12 10:31:51 -07:00
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();
}
}
2020-03-29 17:11:10 -07:00
apps.importApp(req.resource, data, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
2020-12-06 19:38:50 -08:00
function exportApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
apps.exportApp(req.resource, {}, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
2020-03-29 17:11:10 -07:00
function clone(req, res, next) {
2016-06-17 17:12:55 -05:00
assert.strictEqual(typeof req.body, 'object');
2020-03-29 17:11:10 -07:00
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'));
2019-09-16 09:31:34 -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-16 09:31:34 -07:00
2020-03-29 17:11:10 -07:00
apps.clone(req.resource, data, req.user, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
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
});
}
2020-03-29 17:11:10 -07:00
function backup(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
2020-03-29 17:11:10 -07:00
apps.backup(req.resource, function (error, result) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-08-27 20:55:49 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
2020-03-29 17:11:10 -07:00
function uninstall(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
2020-03-29 17:11:10 -07:00
apps.uninstall(req.resource, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-08-27 20:55:49 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
2020-03-29 17:11:10 -07:00
function start(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
2020-03-29 17:11:10 -07:00
apps.start(req.resource, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
2020-03-29 17:11:10 -07:00
function stop(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
2020-03-29 17:11:10 -07:00
apps.stop(req.resource, auditSource.fromRequest(req), function (error, result) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2019-09-08 16:57:08 -07:00
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
2020-03-29 17:11:10 -07:00
function restart(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
2019-12-20 10:29:29 -08:00
2020-03-29 17:11:10 -07: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 }));
});
}
2020-03-29 17:11:10 -07:00
function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
2020-03-29 17:11:10 -07:00
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) {
2019-10-24 18:05:14 -07:00
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) {
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof req.resource, 'object');
2019-01-08 12:10:53 -08:00
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,
2019-01-08 12:10:53 -08:00
follow: true,
format: 'json'
2017-04-19 21:43:29 -07:00
};
2020-03-29 17:11:10 -07:00
apps.getLogs(req.resource, options, function (error, logStream) {
2019-10-24 18:05:14 -07:00
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);
2019-04-24 16:08:30 -07:00
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) {
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof req.resource, 'object');
2019-01-08 12:10:53 -08:00
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'));
2017-04-18 20:32:57 -07:00
var options = {
lines: lines,
follow: false,
2019-01-08 12:10:53 -08:00
format: req.query.format || 'json'
2017-04-18 20:32:57 -07:00
};
2020-03-29 17:11:10 -07:00
apps.getLogs(req.resource, options, function (error, logStream) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
2020-11-09 10:58:43 +01:00
'Content-Disposition': `attachment; filename="${req.resource.id}.log"`,
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no' // disable nginx buffering
});
logStream.pipe(res);
});
}
2016-05-22 21:07:05 -07:00
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) {
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof req.resource, 'object');
var 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'));
}
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
2020-04-08 09:18:58 -07: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'));
2020-03-29 17:11:10 -07:00
apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
req.clearTimeout();
res.sendUpgradeHandshake();
2016-05-22 21:07:05 -07:00
// 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
2017-08-18 17:56:01 -07:00
function execWebSocket(req, res, next) {
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof req.resource, 'object');
2017-08-15 20:00:52 +02:00
var 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'));
2017-08-15 20:00:52 +02:00
}
var columns = req.query.columns ? parseInt(req.query.columns, 10) : null;
if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number'));
2017-08-15 20:00:52 +02:00
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
2017-08-15 20:00:52 +02:00
var tty = req.query.tty === 'true' ? true : false;
2020-04-08 09:18:58 -07:00
// 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'));
2020-03-29 17:11:10 -07:00
apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2017-08-15 20:00:52 +02:00
2017-08-18 17:56:01 -07:00
req.clearTimeout();
2017-08-15 20:00:52 +02:00
2017-08-18 17:56:01 -07:00
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);
2017-08-18 17:56:01 -07:00
});
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);
2017-08-18 17:56:01 -07:00
});
ws.on('message', function (msg) {
duplexStream.write(msg);
});
ws.on('close', function () {
// Clean things up, if any?
});
2017-08-15 20:00:52 +02:00
});
});
}
2016-01-19 13:35:28 +01:00
function listBackups(req, res, next) {
2020-03-29 17:11:10 -07:00
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'));
2020-03-29 17:11:10 -07:00
apps.listBackups(req.resource, page, perPage, function (error, result) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2016-01-19 13:35:28 +01:00
next(new HttpSuccess(200, { backups: result }));
});
}
2017-08-18 20:45:52 -07:00
function uploadFile(req, res, next) {
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof req.resource, 'object');
2017-08-18 20:45:52 -07:00
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'));
2020-03-29 17:11:10 -07:00
apps.uploadFile(req.resource, req.files.file.path, req.query.file, function (error) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2017-08-18 20:45:52 -07:00
next(new HttpSuccess(202, {}));
});
}
function downloadFile(req, res, next) {
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof req.resource, 'object');
2017-08-18 20:45:52 -07:00
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
2020-03-29 17:11:10 -07:00
apps.downloadFile(req.resource, req.query.file, function (error, stream, info) {
2019-10-24 18:05:14 -07:00
if (error) return next(BoxError.toHttpError(error));
2017-08-18 20:45:52 -07:00
2017-08-20 18:44:26 -07:00
var headers = {
2017-08-18 20:45:52 -07:00
'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-18 20:45:52 -07:00
2017-08-20 18:44:26 -07:00
stream.pipe(res);
2017-08-18 20:45:52 -07:00
});
}
2020-04-29 21:55:21 -07:00
2020-10-28 19:42:48 -07:00
function setMounts(req, res, next) {
2020-04-29 21:55:21 -07:00
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
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
}
2020-10-28 19:42:48 -07:00
apps.setMounts(req.resource, req.body.mounts, auditSource.fromRequest(req), function (error, result) {
2020-04-29 21:55:21 -07:00
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}