'use strict'; exports = module.exports = { getApp, listByUser, getAppIcon, install, uninstall, restore, importApp, exportApp, backup, update, getTask, getLogs, getLogStream, listEventlog, listBackups, repair, setAccessRestriction, setOperators, setCrontab, setLabel, setTags, setIcon, setTurn, setRedis, setMemoryLimit, setCpuQuota, setAutomaticBackup, setAutomaticUpdate, setReverseProxyConfig, setCertificate, setDebugMode, setEnvironment, setMailbox, setInbox, setLocation, setStorage, setMounts, setUpstreamUri, stop, start, restart, createExec, startExec, startExecWebSocket, getExec, checkForUpdates, clone, uploadFile, downloadFile, updateBackup, downloadBackup, getGraphs, load }; const apps = require('../apps.js'), appstore = require('../appstore.js'), assert = require('assert'), AuditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), debug = require('debug')('box:routes/apps'), graphs = require('../graphs.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, safe = require('safetydance'), updateChecker = require('../updatechecker.js'), users = require('../users.js'), WebSocket = require('ws'); async function load(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); const [error, result] = await safe(apps.get(req.params.id)); if (error) return next(BoxError.toHttpError(error)); if (!result) return next(new HttpError(404, 'App not found')); req.app = result; next(); } function getApp(req, res, next) { assert.strictEqual(typeof req.app, 'object'); const result = apps.removeInternalFields(req.app); result.accessLevel = apps.accessLevel(req.app, req.user); next(new HttpSuccess(200, result)); } async function listByUser(req, res, next) { assert.strictEqual(typeof req.user, 'object'); let [error, result] = await safe(apps.listByUser(req.user)); if (error) return next(BoxError.toHttpError(error)); result = result.map(r => { const app = apps.removeRestrictedFields(r); app.accessLevel = apps.accessLevel(r, req.user); return app; }); next(new HttpSuccess(200, { apps: result })); } 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 })); if (error) return next(BoxError.toHttpError(error)); res.send(icon); } async function install(req, res, next) { assert.strictEqual(typeof req.body, 'object'); const data = req.body; // 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')); // required if (typeof data.subdomain !== 'string') return next(new HttpError(400, 'subdomain is required')); 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')); // 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')); if ('label' in data && typeof data.label !== 'string') return next(new HttpError(400, 'label must be a string')); 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 ('secondaryDomains' in data) { if (!data.secondaryDomains || typeof data.secondaryDomains !== 'object') return next(new HttpError(400, 'secondaryDomains must be an object')); if (Object.keys(data.secondaryDomains).some(function (key) { return typeof data.secondaryDomains[key].domain !== 'string' || typeof data.secondaryDomains[key].subdomain !== 'string'; })) return next(new HttpError(400, 'secondaryDomain object must contain domain and subdomain strings')); } if ('redirectDomains' in data) { if (!Array.isArray(data.redirectDomains)) return next(new HttpError(400, 'redirectDomains must be an array')); if (data.redirectDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'redirectDomains array must contain objects with domain and subdomain strings')); } 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 data && typeof data.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean')); if ('skipDnsSetup' in data && typeof data.skipDnsSetup !== 'boolean') return next(new HttpError(400, 'skipDnsSetup must be boolean')); if ('enableMailbox' in data && typeof data.enableMailbox !== 'boolean') return next(new HttpError(400, 'enableMailbox must be boolean')); if ('enableTurn' in data && typeof data.enableTurn !== 'boolean') return next(new HttpError(400, 'enableTurn must be boolean')); let [error, result] = await safe(appstore.downloadManifest(data.appStoreId, data.manifest)); if (error) return next(BoxError.toHttpError(error)); if (result.appStoreId === constants.PROXY_APP_APPSTORE_ID && typeof data.upstreamUri !== 'string') return next(new HttpError(400, 'upstreamUri must be a non empty string')); 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')); data.appStoreId = result.appStoreId; data.manifest = result.manifest; [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 })); } async function setAccessRestriction(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function setOperators(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); if (typeof req.body.operators !== 'object') return next(new HttpError(400, 'operators must be an object')); const [error] = await safe(apps.setOperators(req.app, req.body.operators, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function setCrontab(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); if (req.body.crontab !== null && typeof req.body.crontab !== 'string') return next(new HttpError(400, 'crontab must be a string')); const [error] = await safe(apps.setCrontab(req.app, req.body.crontab, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function setLabel(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function setTags(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function setIcon(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function setMemoryLimit(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } function setCpuQuota(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); if (typeof req.body.cpuQuota !== 'number') return next(new HttpError(400, 'cpuQuota is not a number')); apps.setCpuQuota(req.app, req.body.cpuQuota, AuditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } async function setAutomaticBackup(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function setAutomaticUpdate(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function setReverseProxyConfig(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); 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')); if (typeof req.body.hstsPreload !== 'boolean') return next(new HttpError(400, 'hstsPreload must be a boolean')); const [error] = await safe(apps.setReverseProxyConfig(req.app, req.body, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function setCertificate(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); if (typeof req.body.subdomain !== 'string') return next(new HttpError(400, 'subdomain must be string')); // subdomain 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')); 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function setEnvironment(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); if (!req.body.env || typeof req.body.env !== 'object') return next(new HttpError(400, 'env must be an object')); 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')); const [error, result] = await safe(apps.setEnvironment(req.app, req.body.env, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function setDebugMode(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function setMailbox(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); 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')); if ('mailboxDisplayName' in req.body && typeof req.body.mailboxDisplayName !== 'string') return next(new HttpError(400, 'mailboxDisplayName must be a string')); } const [error, result] = await safe(apps.setMailbox(req.app, req.body, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function setInbox(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean')); if (req.body.enable) { if (typeof req.body.inboxName !== 'string') return next(new HttpError(400, 'inboxName must be a string')); if (typeof req.body.inboxDomain !== 'string') return next(new HttpError(400, 'inboxDomain must be a string')); } const [error, result] = await safe(apps.setInbox(req.app, req.body, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function setTurn(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean')); const [error, result] = await safe(apps.setTurn(req.app, req.body.enable, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function setRedis(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean')); const [error, result] = await safe(apps.setRedis(req.app, req.body.enable, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function setLocation(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); if (typeof req.body.subdomain !== 'string') return next(new HttpError(400, 'subdomain must be string')); // subdomain may be an empty string 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 ('secondaryDomains' in req.body) { if (!req.body.secondaryDomains || typeof req.body.secondaryDomains !== 'object') return next(new HttpError(400, 'secondaryDomains must be an object')); if (Object.keys(req.body.secondaryDomains).some(function (key) { return typeof req.body.secondaryDomains[key].domain !== 'string' || typeof req.body.secondaryDomains[key].subdomain !== 'string'; })) return next(new HttpError(400, 'secondaryDomain object must contain domain and subdomain strings')); } if ('redirectDomains' in req.body) { if (!Array.isArray(req.body.redirectDomains)) return next(new HttpError(400, 'redirectDomains must be an array')); if (req.body.redirectDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'redirectDomains array must contain objects with domain and subdomain strings')); } 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')); } 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.setLocation(req.app, req.body, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function setStorage(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); const { storageVolumeId, storageVolumePrefix } = req.body; if (storageVolumeId !== null) { if (typeof storageVolumeId !== 'string') return next(new HttpError(400, 'storageVolumeId must be a string')); if (typeof storageVolumePrefix !== 'string') return next(new HttpError(400, 'storageVolumePrefix must be a string')); } const [error, result] = await safe(apps.setStorage(req.app, storageVolumeId, storageVolumePrefix, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function repair(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); const data = req.body; if ('manifest' in data) { if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object')); 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function restore(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); const data = req.body; 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function importApp(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); const data = req.body; if ('remotePath' in data) { // if not provided, we import in-place if (typeof data.remotePath !== 'string' || !data.remotePath) return next(new HttpError(400, 'remotePath must be non-empty 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 (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 ('encryptedFilenames' in backupConfig && typeof backupConfig.encryptedFilenames !== 'boolean') return next(new HttpError(400, 'encryptedFilenames 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } 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))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function clone(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); const data = req.body; if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string')); if (typeof data.subdomain !== 'string') return next(new HttpError(400, 'subdomain is required')); if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required')); if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object')); if ('secondaryDomains' in data) { if (!data.secondaryDomains || typeof data.secondaryDomains !== 'object') return next(new HttpError(400, 'secondaryDomains must be an object')); if (Object.keys(data.secondaryDomains).some(function (key) { return typeof data.secondaryDomains[key].domain !== 'string' || typeof data.secondaryDomains[key].subdomain !== 'string'; })) return next(new HttpError(400, 'secondaryDomain object must contain domain and subdomain 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')); const [error, result] = await safe(apps.clone(req.app, data, req.user, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(201, { id: result.id, taskId: result.taskId })); } async function backup(req, res, next) { assert.strictEqual(typeof req.app, 'object'); const [error, result] = await safe(apps.backup(req.app, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function uninstall(req, res, next) { assert.strictEqual(typeof req.app, 'object'); const [error, result] = await safe(apps.uninstall(req.app, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function start(req, res, next) { assert.strictEqual(typeof req.app, 'object'); const [error, result] = await safe(apps.start(req.app, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function stop(req, res, next) { assert.strictEqual(typeof req.app, 'object'); const [error, result] = await safe(apps.stop(req.app, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function restart(req, res, next) { assert.strictEqual(typeof req.app, 'object'); const [error, result] = await safe(apps.restart(req.app, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function update(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); const data = req.body; // 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')); let [error, result] = await safe(appstore.downloadManifest(data.appStoreId, data.manifest)); if (error) return next(BoxError.toHttpError(error)); const { appStoreId, manifest } = result; 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; [error, result] = await safe(apps.updateApp(req.app, data, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } // this route is for streaming logs async function getLogStream(req, res, next) { assert.strictEqual(typeof req.app, 'object'); const lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number')); if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream')); const options = { lines: lines, follow: true, format: 'json' }; const [error, logStream] = await safe(apps.getLogs(req.app, options)); 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.destroy()); logStream.on('data', function (data) { const obj = JSON.parse(data); const sse = `data: ${JSON.stringify(obj)}\n\n`; res.write(sse); }); logStream.on('end', res.end.bind(res)); logStream.on('error', res.end.bind(res, null)); } async function getLogs(req, res, next) { assert.strictEqual(typeof req.app, 'object'); const lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number')); const options = { lines, follow: false, format: req.query.format || 'json' }; const [error, logStream] = await safe(apps.getLogs(req.app, options)); 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 }); res.on('close', () => logStream.destroy()); logStream.pipe(res); } function demuxStream(stream, stdin) { let header = null; stream.on('readable', function() { header = header || stream.read(4); while (header !== null) { const length = header.readUInt32BE(0); if (length === 0) { header = null; return stdin.end(); // EOF } const payload = stream.read(length); if (payload === null) break; stdin.write(payload); header = stream.read(4); } }); } async function createExec(req, res, next) { assert.strictEqual(typeof req.app, 'object'); assert.strictEqual(typeof req.body, 'object'); if ('cmd' in req.body) { if (!Array.isArray(req.body.cmd) || req.body.cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1')); } const cmd = req.body.cmd || null; if ('tty' in req.body && typeof req.body.tty !== 'boolean') return next(new HttpError(400, 'tty must be boolean')); const tty = !!req.body.tty; if ('lang' in req.body && typeof req.body.lang !== 'string') return next(new HttpError(400, 'lang must be a string')); 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')); const [error, id] = await safe(apps.createExec(req.app, { cmd, tty, lang: req.body.lang })); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, { id })); } async function startExec(req, res, next) { assert.strictEqual(typeof req.app, 'object'); assert.strictEqual(typeof req.params.execId, 'string'); const columns = req.query.columns ? parseInt(req.query.columns, 10) : null; if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number')); const rows = req.query.rows ? parseInt(req.query.rows, 10) : null; if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number')); 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')); // 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.startExec(req.app, req.params.execId, { rows, columns, tty })); 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(); }); } } async function startExecWebSocket(req, res, next) { assert.strictEqual(typeof req.app, 'object'); assert.strictEqual(typeof req.params.execId, 'string'); const columns = req.query.columns ? parseInt(req.query.columns, 10) : null; if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number')); const rows = req.query.rows ? parseInt(req.query.rows, 10) : null; if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number')); 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.startExec(req.app, req.params.execId, { rows, columns, tty })); 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) { debug('duplexStream error: %o', error); }); duplexStream.on('data', function (data) { if (ws.readyState !== WebSocket.OPEN) return; ws.send(data.toString()); }); ws.on('error', function (error) { debug('websocket error: %o', error); }); ws.on('message', function (msg) { duplexStream.write(msg); }); ws.on('close', function () { // Clean things up, if any? }); }); } async function getExec(req, res, next) { assert.strictEqual(typeof req.app, 'object'); assert.strictEqual(typeof req.params.execId, 'string'); const [error, result] = await safe(apps.getExec(req.app, req.params.execId)); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, result)); // { exitCode, running } } async function listBackups(req, res, next) { assert.strictEqual(typeof req.app, 'object'); const 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')); const 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')); const [error, result] = await safe(apps.listBackups(req.app, page, perPage)); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, { backups: result })); } async function updateBackup(req, res, next) { assert.strictEqual(typeof req.app, 'object'); assert.strictEqual(typeof req.params.backupId, 'string'); assert.strictEqual(typeof req.body, 'object'); const { label, preserveSecs } = req.body; if (typeof label !== 'string') return next(new HttpError(400, 'label must be a string')); if (typeof preserveSecs !== 'number') return next(new HttpError(400, 'preserveSecs must be a number')); const [error] = await safe(apps.updateBackup(req.app, req.params.backupId, { label, preserveSecs })); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function downloadBackup(req, res, next) { assert.strictEqual(typeof req.app, 'object'); assert.strictEqual(typeof req.params.backupId, 'string'); const [error, result] = await safe(apps.getBackupDownloadStream(req.app, req.params.backupId)); if (error) return next(BoxError.toHttpError(error)); res.attachment(result.filename); result.stream.pipe(res); } async 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')); req.clearTimeout(); const [error] = await safe(apps.uploadFile(req.app, req.files.file.path, req.query.file)); safe.fs.unlinkSync(req.files.file.path); // remove file in /tmp if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, {})); } async 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')); req.clearTimeout(); const [error, result] = await safe(apps.downloadFile(req.app, req.query.file)); if (error) return next(BoxError.toHttpError(error)); const { stream, filename, size } = result; const headers = { 'Content-Type': 'application/octet-stream', 'Content-Disposition': `attachment; filename*=utf-8''${encodeURIComponent(filename)}` // RFC 2184 section 4 }; if (size) headers['Content-Length'] = size; res.writeHead(200, headers); stream.pipe(res); } async function setMounts(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); if (!Array.isArray(req.body.mounts)) return next(new HttpError(400, 'mounts should be an array')); 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')); } const [error, result] = await safe(apps.setMounts(req.app, req.body.mounts, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); } async function setUpstreamUri(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); if (req.app.manifest.id !== constants.PROXY_APP_APPSTORE_ID) return next(new HttpError(400, 'upstreamUri can only be set for proxy app')); if (typeof req.body.upstreamUri !== 'string') return next(new HttpError(400, 'upstreamUri must be a string')); const [error] = await safe(apps.setUpstreamUri(req.app, req.body.upstreamUri, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function listEventlog(req, res, next) { assert.strictEqual(typeof req.app, 'object'); const 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')); const 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')); const [error, eventlogs] = await safe(apps.listEventlog(req.app, page, perPage)); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, { eventlogs })); } async function checkForUpdates(req, res, next) { assert.strictEqual(typeof req.app, 'object'); // it can take a while sometimes to get all the app updates one by one req.clearTimeout(); await updateChecker.checkForUpdates({ automatic: false }); // appId argument is ignored for the moment next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() })); } async function getTask(req, res, next) { assert.strictEqual(typeof req.app, 'object'); const [error, result] = await safe(apps.getTask(req.app)); if (error) return next(BoxError.toHttpError(error)); if (result === null) return next(new HttpError(400, 'No active task')); next(new HttpSuccess(200, result)); } async function getGraphs(req, res, next) { assert.strictEqual(typeof req.app, 'object'); if (!req.query.fromMinutes || !parseInt(req.query.fromMinutes)) return next(new HttpError(400, 'fromMinutes must be a number')); const fromMinutes = parseInt(req.query.fromMinutes); const noNullPoints = !!req.query.noNullPoints; const [error, result] = await safe(graphs.getContainerStats(req.app.id, fromMinutes, noNullPoints)); if (error) return next(new HttpError(500, error)); next(new HttpSuccess(200, result)); }