732 lines
29 KiB
JavaScript
732 lines
29 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
getApp: getApp,
|
|
getApps: getApps,
|
|
getAppIcon: getAppIcon,
|
|
installApp: installApp,
|
|
uninstallApp: uninstallApp,
|
|
restoreApp: restoreApp,
|
|
importApp: importApp,
|
|
backupApp: backupApp,
|
|
updateApp: updateApp,
|
|
getLogs: getLogs,
|
|
getLogStream: getLogStream,
|
|
listBackups: listBackups,
|
|
repairApp: repairApp,
|
|
|
|
setAccessRestriction: setAccessRestriction,
|
|
setLabel: setLabel,
|
|
setTags: setTags,
|
|
setIcon: setIcon,
|
|
setMemoryLimit: setMemoryLimit,
|
|
setCpuShares: setCpuShares,
|
|
setAutomaticBackup: setAutomaticBackup,
|
|
setAutomaticUpdate: setAutomaticUpdate,
|
|
setReverseProxyConfig: setReverseProxyConfig,
|
|
setCertificate: setCertificate,
|
|
setDebugMode: setDebugMode,
|
|
setEnvironment: setEnvironment,
|
|
setMailbox: setMailbox,
|
|
setLocation: setLocation,
|
|
setDataDir: setDataDir,
|
|
|
|
stopApp: stopApp,
|
|
startApp: startApp,
|
|
restartApp: restartApp,
|
|
exec: exec,
|
|
execWebSocket: execWebSocket,
|
|
|
|
cloneApp: cloneApp,
|
|
|
|
uploadFile: uploadFile,
|
|
downloadFile: downloadFile
|
|
};
|
|
|
|
var apps = require('../apps.js'),
|
|
assert = require('assert'),
|
|
auditSource = require('../auditsource.js'),
|
|
BoxError = require('../boxerror.js'),
|
|
debug = require('debug')('box:routes/apps'),
|
|
HttpError = require('connect-lastmile').HttpError,
|
|
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
|
safe = require('safetydance'),
|
|
util = require('util'),
|
|
WebSocket = require('ws');
|
|
|
|
function getApp(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
apps.get(req.params.id, function (error, app) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, apps.removeInternalFields(app)));
|
|
});
|
|
}
|
|
|
|
function getApps(req, res, next) {
|
|
assert.strictEqual(typeof req.user, 'object');
|
|
|
|
apps.getAllByUser(req.user, function (error, allApps) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
allApps = allApps.map(apps.removeRestrictedFields);
|
|
|
|
next(new HttpSuccess(200, { apps: allApps }));
|
|
});
|
|
}
|
|
|
|
function getAppIcon(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
apps.getIconPath(req.params.id, { original: req.query.original }, function (error, iconPath) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
res.sendFile(iconPath);
|
|
});
|
|
}
|
|
|
|
function installApp(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
|
|
var 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.location !== 'string') return next(new HttpError(400, 'location 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'));
|
|
|
|
// falsy values in cert and key unset the cert
|
|
if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
|
if (data.cert && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
|
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
|
|
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
|
|
|
|
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
|
|
|
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
|
|
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
|
|
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
|
|
|
|
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
|
|
|
if ('alternateDomains' in data) {
|
|
if (!Array.isArray(data.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
|
|
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
|
|
}
|
|
|
|
if ('env' in data) {
|
|
if (!data.env || typeof data.env !== 'object') return next(new HttpError(400, 'env must be an object'));
|
|
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
|
|
}
|
|
|
|
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
|
|
|
|
apps.install(data, req.user, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function setAccessRestriction(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
if (typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
|
|
|
|
apps.setAccessRestriction(req.params.id, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, {}));
|
|
});
|
|
}
|
|
|
|
function setLabel(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
if (typeof req.body.label !== 'string') return next(new HttpError(400, 'label must be a string'));
|
|
|
|
apps.setLabel(req.params.id, req.body.label, auditSource.fromRequest(req), function (error) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, {}));
|
|
});
|
|
}
|
|
|
|
function setTags(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
if (!Array.isArray(req.body.tags)) return next(new HttpError(400, 'tags must be an array'));
|
|
if (req.body.tags.some((t) => typeof t !== 'string')) return next(new HttpError(400, 'tags array must contain strings'));
|
|
|
|
apps.setTags(req.params.id, req.body.tags, auditSource.fromRequest(req), function (error) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, {}));
|
|
});
|
|
}
|
|
|
|
function setIcon(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
if (req.body.icon !== null && typeof req.body.icon !== 'string') return next(new HttpError(400, 'icon is null or a base-64 image string'));
|
|
|
|
apps.setIcon(req.params.id, req.body.icon, auditSource.fromRequest(req), function (error) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, {}));
|
|
});
|
|
}
|
|
|
|
function setMemoryLimit(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
if (typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
|
|
|
apps.setMemoryLimit(req.params.id, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function setCpuShares(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
if (typeof req.body.cpuShares !== 'number') return next(new HttpError(400, 'cpuShares is not a number'));
|
|
|
|
apps.setCpuShares(req.params.id, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function setAutomaticBackup(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
|
|
|
|
apps.setAutomaticBackup(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, {}));
|
|
});
|
|
}
|
|
|
|
function setAutomaticUpdate(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
|
|
|
|
apps.setAutomaticUpdate(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, {}));
|
|
});
|
|
}
|
|
|
|
function setReverseProxyConfig(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
if (req.body.robotsTxt !== null && typeof req.body.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt is not a string'));
|
|
|
|
if (req.body.csp !== null && typeof req.body.csp !== 'string') return next(new HttpError(400, 'csp is not a string'));
|
|
|
|
apps.setReverseProxyConfig(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, {}));
|
|
});
|
|
}
|
|
|
|
function setCertificate(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, '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'));
|
|
|
|
apps.setCertificate(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, {}));
|
|
});
|
|
}
|
|
|
|
function setEnvironment(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
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'));
|
|
|
|
apps.setEnvironment(req.params.id, req.body.env, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function setDebugMode(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
if (req.body.debugMode !== null && typeof req.body.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
|
|
|
apps.setDebugMode(req.params.id, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function setMailbox(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
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'));
|
|
|
|
apps.setMailbox(req.params.id, req.body.mailboxName, req.body.mailboxDomain, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function setLocation(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
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'));
|
|
|
|
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'));
|
|
}
|
|
|
|
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
|
|
|
|
apps.setLocation(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function setDataDir(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
if (req.body.dataDir !== null && typeof req.body.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
|
|
|
|
apps.setDataDir(req.params.id, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function repairApp(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
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 ('dockerImage' in data) {
|
|
if (!data.dockerImage || typeof data.dockerImage !== 'string') return next(new HttpError(400, 'dockerImage must be a string'));
|
|
}
|
|
|
|
apps.repair(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function restoreApp(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
var data = req.body;
|
|
|
|
if (!data.backupId || typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be non-empty string'));
|
|
|
|
apps.restore(req.params.id, data.backupId, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function importApp(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
var data = req.body;
|
|
|
|
if ('backupId' in data) { // if not provided, we import in-place
|
|
if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string'));
|
|
if (typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string'));
|
|
|
|
if ('backupConfig' in data && typeof data.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig must be an object'));
|
|
|
|
const backupConfig = req.body.backupConfig;
|
|
|
|
if (req.body.backupConfig) {
|
|
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
|
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
|
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
|
|
|
|
// testing backup config can take sometime
|
|
req.clearTimeout();
|
|
}
|
|
}
|
|
|
|
apps.importApp(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function cloneApp(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
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'));
|
|
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 ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
|
|
|
|
apps.clone(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(201, { id: result.id, taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function backupApp(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
apps.backup(req.params.id, function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function uninstallApp(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function startApp(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
apps.start(req.params.id, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function stopApp(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
apps.stop(req.params.id, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function restartApp(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
apps.restart(req.params.id, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
function updateApp(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
|
|
var 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'));
|
|
|
|
apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
|
});
|
|
}
|
|
|
|
// this route is for streaming logs
|
|
function getLogStream(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
var 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'));
|
|
|
|
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'));
|
|
|
|
var options = {
|
|
lines: lines,
|
|
follow: true,
|
|
format: 'json'
|
|
};
|
|
|
|
apps.getLogs(req.params.id, 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.params.id, 'string');
|
|
|
|
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10;
|
|
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.params.id, options, function (error, logStream) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
res.writeHead(200, {
|
|
'Content-Type': 'application/x-logs',
|
|
'Content-Disposition': 'attachment; filename="log.txt"',
|
|
'Cache-Control': 'no-cache',
|
|
'X-Accel-Buffering': 'no' // disable nginx buffering
|
|
});
|
|
logStream.pipe(res);
|
|
});
|
|
}
|
|
|
|
function demuxStream(stream, stdin) {
|
|
var header = null;
|
|
|
|
stream.on('readable', function() {
|
|
header = header || stream.read(4);
|
|
|
|
while (header !== null) {
|
|
var length = header.readUInt32BE(0);
|
|
if (length === 0) {
|
|
header = null;
|
|
return stdin.end(); // EOF
|
|
}
|
|
|
|
var payload = stream.read(length);
|
|
|
|
if (payload === null) break;
|
|
stdin.write(payload);
|
|
header = stream.read(4);
|
|
}
|
|
});
|
|
}
|
|
|
|
function exec(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
var cmd = null;
|
|
if (req.query.cmd) {
|
|
cmd = safe.JSON.parse(req.query.cmd);
|
|
if (!util.isArray(cmd) || cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1'));
|
|
}
|
|
|
|
var columns = req.query.columns ? parseInt(req.query.columns, 10) : null;
|
|
if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number'));
|
|
|
|
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
|
|
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
|
|
|
|
var tty = req.query.tty === 'true' ? true : false;
|
|
|
|
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
|
|
|
|
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(); });
|
|
}
|
|
});
|
|
}
|
|
|
|
function execWebSocket(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
var cmd = null;
|
|
if (req.query.cmd) {
|
|
cmd = safe.JSON.parse(req.query.cmd);
|
|
if (!util.isArray(cmd) || cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1'));
|
|
}
|
|
|
|
var columns = req.query.columns ? parseInt(req.query.columns, 10) : null;
|
|
if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number'));
|
|
|
|
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
|
|
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
|
|
|
|
var tty = req.query.tty === 'true' ? true : false;
|
|
|
|
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
req.clearTimeout();
|
|
|
|
res.handleUpgrade(function (ws) {
|
|
duplexStream.on('end', function () { ws.close(); });
|
|
duplexStream.on('close', function () { ws.close(); });
|
|
duplexStream.on('error', function (error) {
|
|
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?
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function listBackups(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
|
|
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
|
|
|
|
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
|
|
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
|
|
|
|
apps.listBackups(page, perPage, req.params.id, function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, { backups: result }));
|
|
});
|
|
}
|
|
|
|
function uploadFile(req, res, next) {
|
|
assert.strictEqual(typeof req.params.id, 'string');
|
|
|
|
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.params.id, 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.params.id, 'string');
|
|
|
|
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
|
|
|
|
apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
var headers = {
|
|
'Content-Type': 'application/octet-stream',
|
|
'Content-Disposition': `attachment; filename*=utf-8''${encodeURIComponent(info.filename)}` // RFC 2184 section 4
|
|
};
|
|
if (info.size) headers['Content-Length'] = info.size;
|
|
|
|
res.writeHead(200, headers);
|
|
|
|
stream.pipe(res);
|
|
});
|
|
}
|