diff --git a/src/apps.js b/src/apps.js index 9c0e59e9f..2efcb1ef3 100644 --- a/src/apps.js +++ b/src/apps.js @@ -419,11 +419,13 @@ function getIconUrlSync(app) { return null; } -function getIconPath(appId, options, callback) { - assert.strictEqual(typeof appId, 'string'); +function getIconPath(app, options, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); + const appId = app.id; + if (!options.original) { const userIconPath = `${paths.APP_ICONS_DIR}/${appId}.user.png`; if (safe.fs.existsSync(userIconPath)) return callback(null, userIconPath); @@ -822,520 +824,472 @@ function install(data, auditSource, callback) { }); } -function setAccessRestriction(appId, accessRestriction, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function setAccessRestriction(app, accessRestriction, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof accessRestriction, 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - get(appId, function (error, app) { + const appId = app.id; + let error = validateAccessRestriction(accessRestriction); + if (error) return callback(error); + + appdb.update(appId, { accessRestriction: accessRestriction }, function (error) { if (error) return callback(error); - error = validateAccessRestriction(accessRestriction); - if (error) return callback(error); - - appdb.update(appId, { accessRestriction: accessRestriction }, function (error) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, accessRestriction }); - - callback(); - }); - }); -} - -function setLabel(appId, label, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof label, 'string'); - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - get(appId, function (error, app) { - if (error) return callback(error); - - error = validateLabel(label); - if (error) return callback(error); - - appdb.update(appId, { label: label }, function (error) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, label }); - - callback(); - }); - }); -} - -function setTags(appId, tags, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); - assert(Array.isArray(tags)); - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - get(appId, function (error, app) { - if (error) return callback(error); - - error = validateTags(tags); - if (error) return callback(error); - - appdb.update(appId, { tags: tags }, function (error) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, tags }); - - callback(); - }); - }); -} - -function setIcon(appId, icon, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); - assert(icon === null || typeof icon === 'string'); - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - get(appId, function (error, app) { - if (error) return callback(error); - - if (icon) { - if (!validator.isBase64(icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' })); - - if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(icon, 'base64'))) { - return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message)); - } - } else { - safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png')); - } - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, icon }); + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, accessRestriction }); callback(); }); } -function setMemoryLimit(appId, memoryLimit, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function setLabel(app, label, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof label, 'string'); + assert.strictEqual(typeof auditSource, 'object'); + assert.strictEqual(typeof callback, 'function'); + + const appId = app.id; + let error = validateLabel(label); + if (error) return callback(error); + + appdb.update(appId, { label: label }, function (error) { + if (error) return callback(error); + + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, label }); + + callback(); + }); +} + +function setTags(app, tags, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); + assert(Array.isArray(tags)); + assert.strictEqual(typeof auditSource, 'object'); + assert.strictEqual(typeof callback, 'function'); + + const appId = app.id; + let error = validateTags(tags); + if (error) return callback(error); + + appdb.update(appId, { tags: tags }, function (error) { + if (error) return callback(error); + + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, tags }); + + callback(); + }); +} + +function setIcon(app, icon, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); + assert(icon === null || typeof icon === 'string'); + assert.strictEqual(typeof auditSource, 'object'); + assert.strictEqual(typeof callback, 'function'); + + const appId = app.id; + + if (icon) { + if (!validator.isBase64(icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' })); + + if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(icon, 'base64'))) { + return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message)); + } + } else { + safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png')); + } + + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, icon }); + + callback(); +} + +function setMemoryLimit(app, memoryLimit, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof memoryLimit, 'number'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - get(appId, function (error, app) { + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_RESIZE); + if (error) return callback(error); + + error = validateMemoryLimit(app.manifest, memoryLimit); + if (error) return callback(error); + + const task = { + args: {}, + values: { memoryLimit } + }; + addTask(appId, exports.ISTATE_PENDING_RESIZE, task, function (error, result) { if (error) return callback(error); - error = checkAppState(app, exports.ISTATE_PENDING_RESIZE); - if (error) return callback(error); + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, memoryLimit, taskId: result.taskId }); - error = validateMemoryLimit(app.manifest, memoryLimit); - if (error) return callback(error); - - const task = { - args: {}, - values: { memoryLimit } - }; - addTask(appId, exports.ISTATE_PENDING_RESIZE, task, function (error, result) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, memoryLimit, taskId: result.taskId }); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); } -function setCpuShares(appId, cpuShares, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function setCpuShares(app, cpuShares, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof cpuShares, 'number'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - get(appId, function (error, app) { + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_RESIZE); + if (error) return callback(error); + + error = validateCpuShares(cpuShares); + if (error) return callback(error); + + const task = { + args: {}, + values: { cpuShares } + }; + addTask(appId, exports.ISTATE_PENDING_RESIZE, task, function (error, result) { if (error) return callback(error); - error = checkAppState(app, exports.ISTATE_PENDING_RESIZE); - if (error) return callback(error); + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cpuShares, taskId: result.taskId }); - error = validateCpuShares(cpuShares); - if (error) return callback(error); - - const task = { - args: {}, - values: { cpuShares } - }; - addTask(appId, exports.ISTATE_PENDING_RESIZE, task, function (error, result) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cpuShares, taskId: result.taskId }); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); } -function setEnvironment(appId, env, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function setEnvironment(app, env, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof env, 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - get(appId, function (error, app) { + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER); + if (error) return callback(error); + + error = validateEnv(env); + if (error) return callback(error); + + const task = { + args: {}, + values: { env } + }; + addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) { if (error) return callback(error); - error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER); - if (error) return callback(error); + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, env, taskId: result.taskId }); - error = validateEnv(env); - if (error) return callback(error); - - const task = { - args: {}, - values: { env } - }; - addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, env, taskId: result.taskId }); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); } -function setDebugMode(appId, debugMode, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function setDebugMode(app, debugMode, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof debugMode, 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - get(appId, function (error, app) { + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_DEBUG); + if (error) return callback(error); + + error = validateDebugMode(debugMode); + if (error) return callback(error); + + const task = { + args: {}, + values: { debugMode } + }; + addTask(appId, exports.ISTATE_PENDING_DEBUG, task, function (error, result) { if (error) return callback(error); - error = checkAppState(app, exports.ISTATE_PENDING_DEBUG); - if (error) return callback(error); + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, debugMode, taskId: result.taskId }); - error = validateDebugMode(debugMode); - if (error) return callback(error); - - const task = { - args: {}, - values: { debugMode } - }; - addTask(appId, exports.ISTATE_PENDING_DEBUG, task, function (error, result) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, debugMode, taskId: result.taskId }); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); } -function setMailbox(appId, mailboxName, mailboxDomain, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function setMailbox(app, mailboxName, mailboxDomain, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert(mailboxName === null || typeof mailboxName === 'string'); assert.strictEqual(typeof mailboxDomain, 'string'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - get(appId, function (error, app) { + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER); + if (error) return callback(error); + + mail.getDomain(mailboxDomain, function (error) { if (error) return callback(error); - error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER); - if (error) return callback(error); + if (mailboxName) { + error = mail.validateName(mailboxName); + if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' })); + } else { + mailboxName = mailboxNameForLocation(app.location, app.manifest); + } - mail.getDomain(mailboxDomain, function (error) { + const task = { + args: {}, + values: { mailboxName, mailboxDomain } + }; + addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) { if (error) return callback(error); - if (mailboxName) { - error = mail.validateName(mailboxName); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' })); - } else { - mailboxName = mailboxNameForLocation(app.location, app.manifest); - } + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mailboxName, taskId: result.taskId }); - const task = { - args: {}, - values: { mailboxName, mailboxDomain } - }; - addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mailboxName, taskId: result.taskId }); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); }); } -function setAutomaticBackup(appId, enable, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function setAutomaticBackup(app, enable, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof enable, 'boolean'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - get(appId, function (error, app) { + const appId = app.id; + appdb.update(appId, { enableBackup: enable }, function (error) { if (error) return callback(error); - appdb.update(appId, { enableBackup: enable }, function (error) { - if (error) return callback(error); + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableBackup: enable }); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableBackup: enable }); - - callback(); - }); + callback(); }); } -function setAutomaticUpdate(appId, enable, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function setAutomaticUpdate(app, enable, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof enable, 'boolean'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - get(appId, function (error, app) { + const appId = app.id; + appdb.update(appId, { enableAutomaticUpdate: enable }, function (error) { if (error) return callback(error); - appdb.update(appId, { enableAutomaticUpdate: enable }, function (error) { - if (error) return callback(error); + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableAutomaticUpdate: enable }); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableAutomaticUpdate: enable }); - - callback(); - }); + callback(); }); } -function setReverseProxyConfig(appId, reverseProxyConfig, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function setReverseProxyConfig(app, reverseProxyConfig, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof reverseProxyConfig, 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); reverseProxyConfig = _.extend({ robotsTxt: null, csp: null }, reverseProxyConfig); - get(appId, function (error, app) { + const appId = app.id; + let error = validateCsp(reverseProxyConfig.csp); + if (error) return callback(error); + + error = validateRobotsTxt(reverseProxyConfig.robotsTxt); + if (error) return callback(error); + + reverseProxy.writeAppConfig(_.extend({}, app, { reverseProxyConfig }), function (error) { if (error) return callback(error); - error = validateCsp(reverseProxyConfig.csp); - if (error) return callback(error); - - error = validateRobotsTxt(reverseProxyConfig.robotsTxt); - if (error) return callback(error); - - reverseProxy.writeAppConfig(_.extend({}, app, { reverseProxyConfig }), function (error) { + appdb.update(appId, { reverseProxyConfig }, function (error) { if (error) return callback(error); - appdb.update(appId, { reverseProxyConfig }, function (error) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, reverseProxyConfig }); - - callback(); - }); - }); - }); -} - -function setCertificate(appId, bundle, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); - assert(bundle && typeof bundle === 'object'); - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - get(appId, function (error, app) { - if (error) return callback(error); - - domains.get(app.domain, function (error, domainObject) { - if (error) return callback(error); - - if (bundle.cert && bundle.key) { - error = reverseProxy.validateCertificate(app.location, domainObject, { cert: bundle.cert, key: bundle.key }); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'cert' })); - } - - error = reverseProxy.setAppCertificateSync(app.location, domainObject, { cert: bundle.cert, key: bundle.key }); - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cert: bundle.cert, key: bundle.key }); + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, reverseProxyConfig }); callback(); }); }); } -function setLocation(appId, data, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function setCertificate(app, bundle, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); + assert(bundle && typeof bundle === 'object'); + assert.strictEqual(typeof auditSource, 'object'); + assert.strictEqual(typeof callback, 'function'); + + const appId = app.id; + + domains.get(app.domain, function (error, domainObject) { + if (error) return callback(error); + + if (bundle.cert && bundle.key) { + error = reverseProxy.validateCertificate(app.location, domainObject, { cert: bundle.cert, key: bundle.key }); + if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'cert' })); + } + + error = reverseProxy.setAppCertificateSync(app.location, domainObject, { cert: bundle.cert, key: bundle.key }); + if (error) return callback(error); + + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cert: bundle.cert, key: bundle.key }); + + callback(); + }); +} + +function setLocation(app, data, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - get(appId, function (error, app) { + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_LOCATION_CHANGE); + if (error) return callback(error); + + let values = { + location: data.location.toLowerCase(), + domain: data.domain.toLowerCase(), + // these are intentionally reset, if not set + portBindings: null, + alternateDomains: [] + }; + + if ('portBindings' in data) { + error = validatePortBindings(data.portBindings, app.manifest); if (error) return callback(error); - error = checkAppState(app, exports.ISTATE_PENDING_LOCATION_CHANGE); - if (error) return callback(error); + values.portBindings = translatePortBindings(data.portBindings || null, app.manifest); + } - let values = { - location: data.location.toLowerCase(), - domain: data.domain.toLowerCase(), - // these are intentionally reset, if not set - portBindings: null, - alternateDomains: [] - }; + // move the mailbox name to match the new location + if (app.mailboxName.endsWith('.app')) values.mailboxName = mailboxNameForLocation(values.location, app.manifest); - if ('portBindings' in data) { - error = validatePortBindings(data.portBindings, app.manifest); - if (error) return callback(error); + if ('alternateDomains' in data) { + values.alternateDomains = data.alternateDomains; + } - values.portBindings = translatePortBindings(data.portBindings || null, app.manifest); - } + const locations = [{subdomain: values.location, domain: values.domain}].concat(values.alternateDomains); - // move the mailbox name to match the new location - if (app.mailboxName.endsWith('.app')) values.mailboxName = mailboxNameForLocation(values.location, app.manifest); - - if ('alternateDomains' in data) { - values.alternateDomains = data.alternateDomains; - } - - const locations = [{subdomain: values.location, domain: values.domain}].concat(values.alternateDomains); - - validateLocations(locations, function (error, domainObjectMap) { - if (error) return callback(error); - - const task = { - args: { - oldConfig: _.pick(app, 'location', 'domain', 'fqdn', 'alternateDomains', 'portBindings'), - overwriteDns: !!data.overwriteDns - }, - values - }; - addTask(appId, exports.ISTATE_PENDING_LOCATION_CHANGE, task, function (error, result) { - if (error && error.reason === BoxError.ALREADY_EXISTS) error = getDuplicateErrorDetails(error.message, locations, domainObjectMap, data.portBindings); - if (error) return callback(error); - - values.fqdn = domains.fqdn(values.location, domainObjectMap[values.domain]); - values.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, _.extend({ appId, app, taskId: result.taskId }, values)); - - callback(null, { taskId: result.taskId }); - }); - }); - }); -} - -function setDataDir(appId, dataDir, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); - assert(dataDir === null || typeof dataDir === 'string'); - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - get(appId, function (error, app) { - if (error) return callback(error); - - error = checkAppState(app, exports.ISTATE_PENDING_DATA_DIR_MIGRATION); - if (error) return callback(error); - - error = validateDataDir(dataDir); + validateLocations(locations, function (error, domainObjectMap) { if (error) return callback(error); const task = { - args: { newDataDir: dataDir }, - values: { } + args: { + oldConfig: _.pick(app, 'location', 'domain', 'fqdn', 'alternateDomains', 'portBindings'), + overwriteDns: !!data.overwriteDns + }, + values }; - addTask(appId, exports.ISTATE_PENDING_DATA_DIR_MIGRATION, task, function (error, result) { + addTask(appId, exports.ISTATE_PENDING_LOCATION_CHANGE, task, function (error, result) { + if (error && error.reason === BoxError.ALREADY_EXISTS) error = getDuplicateErrorDetails(error.message, locations, domainObjectMap, data.portBindings); if (error) return callback(error); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, dataDir, taskId: result.taskId }); + values.fqdn = domains.fqdn(values.location, domainObjectMap[values.domain]); + values.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, _.extend({ appId, app, taskId: result.taskId }, values)); callback(null, { taskId: result.taskId }); }); }); } -function update(appId, data, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function setDataDir(app, dataDir, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); + assert(dataDir === null || typeof dataDir === 'string'); + assert.strictEqual(typeof auditSource, 'object'); + assert.strictEqual(typeof callback, 'function'); + + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_DATA_DIR_MIGRATION); + if (error) return callback(error); + + error = validateDataDir(dataDir); + if (error) return callback(error); + + const task = { + args: { newDataDir: dataDir }, + values: { } + }; + addTask(appId, exports.ISTATE_PENDING_DATA_DIR_MIGRATION, task, function (error, result) { + if (error) return callback(error); + + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, dataDir, taskId: result.taskId }); + + callback(null, { taskId: result.taskId }); + }); +} + +function update(app, data, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert(data && typeof data === 'object'); assert.strictEqual(data.manifest && typeof data.manifest === 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - debug(`update: id:${appId}`); - const skipBackup = !!data.skipBackup, - manifest = data.manifest; + manifest = data.manifest, + appId = app.id; - get(appId, function (error, app) { - if (error) return callback(error); + let error = checkAppState(app, exports.ISTATE_PENDING_UPDATE); + if (error) return callback(error); - error = checkAppState(app, exports.ISTATE_PENDING_UPDATE); - if (error) return callback(error); + if (app.runState === exports.RSTATE_STOPPED) return callback(new BoxError(BoxError.BAD_STATE, 'Stopped apps cannot be updated')); - if (app.runState === exports.RSTATE_STOPPED) return callback(new BoxError(BoxError.BAD_STATE, 'Stopped apps cannot be updated')); + error = manifestFormat.parse(manifest); + if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Manifest error:' + error.message)); - error = manifestFormat.parse(manifest); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Manifest error:' + error.message)); + error = checkManifestConstraints(manifest); + if (error) return callback(error); - error = checkManifestConstraints(manifest); - if (error) return callback(error); + var updateConfig = { skipBackup, manifest }; - var updateConfig = { skipBackup, manifest }; + // prevent user from installing a app with different manifest id over an existing app + // this allows cloudron install -f --app for an app installed from the appStore + if (app.manifest.id !== updateConfig.manifest.id) { + if (!data.force) return callback(new BoxError(BoxError.BAD_FIELD, 'manifest id does not match. force to override')); + // clear appStoreId so that this app does not get updates anymore + updateConfig.appStoreId = ''; + } - // prevent user from installing a app with different manifest id over an existing app - // this allows cloudron install -f --app for an app installed from the appStore - if (app.manifest.id !== updateConfig.manifest.id) { - if (!data.force) return callback(new BoxError(BoxError.BAD_FIELD, 'manifest id does not match. force to override')); - // clear appStoreId so that this app does not get updates anymore - updateConfig.appStoreId = ''; - } + // suffix '0' if prerelease is missing for semver.lte to work as expected + const currentVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`; + const updateVersion = semver.prerelease(updateConfig.manifest.version) ? updateConfig.manifest.version : `${updateConfig.manifest.version}-0`; + if (app.appStoreId !== '' && semver.lte(updateVersion, currentVersion)) { + if (!data.force) return callback(new BoxError(BoxError.BAD_FIELD, 'Downgrades are not permitted for apps installed from AppStore. force to override')); + } - // suffix '0' if prerelease is missing for semver.lte to work as expected - const currentVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`; - const updateVersion = semver.prerelease(updateConfig.manifest.version) ? updateConfig.manifest.version : `${updateConfig.manifest.version}-0`; - if (app.appStoreId !== '' && semver.lte(updateVersion, currentVersion)) { - if (!data.force) return callback(new BoxError(BoxError.BAD_FIELD, 'Downgrades are not permitted for apps installed from AppStore. force to override')); - } + if ('icon' in data) { + if (data.icon) { + if (!validator.isBase64(data.icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' })); - if ('icon' in data) { - if (data.icon) { - if (!validator.isBase64(data.icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' })); - - if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) { - return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message)); - } - } else { - safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png')); + if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) { + return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message)); } + } else { + safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png')); } + } - // do not update apps in debug mode - if (app.debugMode && !data.force) return callback(new BoxError(BoxError.BAD_STATE, 'debug mode enabled. force to override')); + // do not update apps in debug mode + if (app.debugMode && !data.force) return callback(new BoxError(BoxError.BAD_STATE, 'debug mode enabled. force to override')); - // Ensure we update the memory limit in case the new app requires more memory as a minimum - // 0 and -1 are special updateConfig for memory limit indicating unset and unlimited - if (app.memoryLimit > 0 && updateConfig.manifest.memoryLimit && app.memoryLimit < updateConfig.manifest.memoryLimit) { - updateConfig.memoryLimit = updateConfig.manifest.memoryLimit; - } + // Ensure we update the memory limit in case the new app requires more memory as a minimum + // 0 and -1 are special updateConfig for memory limit indicating unset and unlimited + if (app.memoryLimit > 0 && updateConfig.manifest.memoryLimit && app.memoryLimit < updateConfig.manifest.memoryLimit) { + updateConfig.memoryLimit = updateConfig.manifest.memoryLimit; + } - const task = { - args: { updateConfig }, - values: {} - }; - addTask(appId, exports.ISTATE_PENDING_UPDATE, task, function (error, result) { - if (error) return callback(error); + const task = { + args: { updateConfig }, + values: {} + }; + addTask(appId, exports.ISTATE_PENDING_UPDATE, task, function (error, result) { + if (error) return callback(error); - eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId, app, skipBackup, toManifest: manifest, fromManifest: app.manifest, force: data.force, taskId: result.taskId }); + eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId, app, skipBackup, toManifest: manifest, fromManifest: app.manifest, force: data.force, taskId: result.taskId }); - // clear update indicator, if update fails, it will come back through the update checker - updateChecker.resetAppUpdateInfo(appId); + // clear update indicator, if update fails, it will come back through the update checker + updateChecker.resetAppUpdateInfo(appId); - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); } -function getLogs(appId, options, callback) { - assert.strictEqual(typeof appId, 'string'); +function getLogs(app, options, callback) { + assert.strictEqual(typeof app, 'object'); assert(options && typeof options === 'object'); assert.strictEqual(typeof callback, 'function'); @@ -1343,198 +1297,181 @@ function getLogs(appId, options, callback) { assert.strictEqual(typeof options.format, 'string'); assert.strictEqual(typeof options.follow, 'boolean'); - debug('Getting logs for %s', appId); + const appId = app.id; - get(appId, function (error, app) { - if (error) return callback(error); + var lines = options.lines === -1 ? '+1' : options.lines, + format = options.format || 'json', + follow = options.follow; - var lines = options.lines === -1 ? '+1' : options.lines, - format = options.format || 'json', - follow = options.follow; + assert.strictEqual(typeof format, 'string'); - assert.strictEqual(typeof format, 'string'); + var args = [ '--lines=' + lines ]; + if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs + args.push(path.join(paths.LOG_DIR, appId, 'apptask.log')); + args.push(path.join(paths.LOG_DIR, appId, 'app.log')); + if (app.manifest.addons && app.manifest.addons.redis) args.push(path.join(paths.LOG_DIR, `redis-${appId}/app.log`)); - var args = [ '--lines=' + lines ]; - if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs - args.push(path.join(paths.LOG_DIR, appId, 'apptask.log')); - args.push(path.join(paths.LOG_DIR, appId, 'app.log')); - if (app.manifest.addons && app.manifest.addons.redis) args.push(path.join(paths.LOG_DIR, `redis-${appId}/app.log`)); + var cp = spawn('/usr/bin/tail', args); - var cp = spawn('/usr/bin/tail', args); + var transformStream = split(function mapper(line) { + if (format !== 'json') return line + '\n'; - var transformStream = split(function mapper(line) { - if (format !== 'json') return line + '\n'; + var data = line.split(' '); // logs are + var timestamp = (new Date(data[0])).getTime(); + if (isNaN(timestamp)) timestamp = 0; + var message = line.slice(data[0].length+1); - var data = line.split(' '); // logs are - var timestamp = (new Date(data[0])).getTime(); - if (isNaN(timestamp)) timestamp = 0; - var message = line.slice(data[0].length+1); + // ignore faulty empty logs + if (!timestamp && !message) return; - // ignore faulty empty logs - if (!timestamp && !message) return; - - return JSON.stringify({ - realtimeTimestamp: timestamp * 1000, - message: message, - source: appId - }) + '\n'; - }); - - transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process - - cp.stdout.pipe(transformStream); - - return callback(null, transformStream); + return JSON.stringify({ + realtimeTimestamp: timestamp * 1000, + message: message, + source: appId + }) + '\n'; }); + + transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process + + cp.stdout.pipe(transformStream); + + return callback(null, transformStream); } // does a re-configure when called from most states. for install/clone errors, it re-installs with an optional manifest // re-configure can take a dockerImage but not a manifest because re-configure does not clean up addons -function repair(appId, data, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function repair(app, data, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof data, 'object'); // { manifest } assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - debug('Will repair app with id:%s', appId); + const appId = app.id; + let errorState = (app.error && app.error.installationState) || exports.ISTATE_PENDING_CONFIGURE; - get(appId, function (error, app) { - if (error) return callback(error); + const task = { + args: {}, + values: {}, + requiredState: null + }; - let errorState = (app.error && app.error.installationState) || exports.ISTATE_PENDING_CONFIGURE; + // maybe split this into a separate route like reinstall? + if (errorState === exports.ISTATE_PENDING_INSTALL || errorState === exports.ISTATE_PENDING_CLONE) { + task.args = { overwriteDns: true }; + if (data.manifest) { + let error = manifestFormat.parse(data.manifest); + if (error) return callback(new BoxError(BoxError.BAD_FIELD, `manifest error: ${error.message}`)); - const task = { - args: {}, - values: {}, - requiredState: null - }; - - // maybe split this into a separate route like reinstall? - if (errorState === exports.ISTATE_PENDING_INSTALL || errorState === exports.ISTATE_PENDING_CLONE) { - task.args = { overwriteDns: true }; - if (data.manifest) { - error = manifestFormat.parse(data.manifest); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, `manifest error: ${error.message}`)); - - error = checkManifestConstraints(data.manifest); - if (error) return callback(error); - - task.values.manifest = data.manifest; - task.args.oldManifest = app.manifest; - } - } else { - errorState = exports.ISTATE_PENDING_CONFIGURE; - if (data.dockerImage) { - let newManifest = _.extend({}, app.manifest, { dockerImage: data.dockerImage }); - task.values.manifest = newManifest; - } - } - - addTask(appId, errorState, task, function (error, result) { + error = checkManifestConstraints(data.manifest); if (error) return callback(error); - eventlog.add(eventlog.ACTION_APP_REPAIR, auditSource, { app, taskId: result.taskId }); + task.values.manifest = data.manifest; + task.args.oldManifest = app.manifest; + } + } else { + errorState = exports.ISTATE_PENDING_CONFIGURE; + if (data.dockerImage) { + let newManifest = _.extend({}, app.manifest, { dockerImage: data.dockerImage }); + task.values.manifest = newManifest; + } + } + + addTask(appId, errorState, task, function (error, result) { + if (error) return callback(error); + + eventlog.add(eventlog.ACTION_APP_REPAIR, auditSource, { app, taskId: result.taskId }); + + callback(null, { taskId: result.taskId }); + }); +} + +function restore(app, backupId, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof backupId, 'string'); + assert.strictEqual(typeof auditSource, 'object'); + assert.strictEqual(typeof callback, 'function'); + + const appId = app.id; + + let error = checkAppState(app, exports.ISTATE_PENDING_RESTORE); + if (error) return callback(error); + + // for empty or null backupId, use existing manifest to mimic a reinstall + var func = backupId ? backups.get.bind(null, backupId) : function (next) { return next(null, { manifest: app.manifest }); }; + + func(function (error, backupInfo) { + if (error) return callback(error); + + if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest')); + + // re-validate because this new box version may not accept old configs + error = checkManifestConstraints(backupInfo.manifest); + if (error) return callback(error); + + const restoreConfig = { backupId, backupFormat: backupInfo.format }; + + const task = { + args: { + restoreConfig, + oldManifest: app.manifest, + overwriteDns: true + }, + values: { + manifest: backupInfo.manifest + } + }; + addTask(appId, exports.ISTATE_PENDING_RESTORE, task, function (error, result) { + if (error) return callback(error); + + eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest, taskId: result.taskId }); callback(null, { taskId: result.taskId }); }); }); } -function restore(appId, backupId, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof backupId, 'string'); - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - debug('Will restore app with id:%s', appId); - - get(appId, function (error, app) { - if (error) return callback(error); - - error = checkAppState(app, exports.ISTATE_PENDING_RESTORE); - if (error) return callback(error); - - // for empty or null backupId, use existing manifest to mimic a reinstall - var func = backupId ? backups.get.bind(null, backupId) : function (next) { return next(null, { manifest: app.manifest }); }; - - func(function (error, backupInfo) { - if (error) return callback(error); - - if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest')); - - // re-validate because this new box version may not accept old configs - error = checkManifestConstraints(backupInfo.manifest); - if (error) return callback(error); - - const restoreConfig = { backupId, backupFormat: backupInfo.format }; - - const task = { - args: { - restoreConfig, - oldManifest: app.manifest, - overwriteDns: true - }, - values: { - manifest: backupInfo.manifest - } - }; - addTask(appId, exports.ISTATE_PENDING_RESTORE, task, function (error, result) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest, taskId: result.taskId }); - - callback(null, { taskId: result.taskId }); - }); - }); - }); -} - -function importApp(appId, data, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function importApp(app, data, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - debug('Will import app with id:%s', appId); + const appId = app.id; - get(appId, function (error, app) { + // all fields are optional + data.backupId = data.backupId || null; + data.backupFormat = data.backupFormat || null; + data.backupConfig = data.backupConfig || null; + const { backupId, backupFormat, backupConfig } = data; + + let error = backupFormat ? validateBackupFormat(backupFormat) : null; + if (error) return callback(error); + + error = checkAppState(app, exports.ISTATE_PENDING_RESTORE); + if (error) return callback(error); + + // TODO: make this smarter to do a read-only test and check if the file exists in the storage backend + const testBackupConfig = backupConfig ? backups.testProviderConfig.bind(null, backupConfig) : (next) => next(); + + testBackupConfig(function (error) { if (error) return callback(error); - // all fields are optional - data.backupId = data.backupId || null; - data.backupFormat = data.backupFormat || null; - data.backupConfig = data.backupConfig || null; - const { backupId, backupFormat, backupConfig } = data; + const restoreConfig = { backupId, backupFormat, backupConfig }; - error = backupFormat ? validateBackupFormat(backupFormat) : null; - if (error) return callback(error); - - error = checkAppState(app, exports.ISTATE_PENDING_RESTORE); - if (error) return callback(error); - - // TODO: make this smarter to do a read-only test and check if the file exists in the storage backend - const testBackupConfig = backupConfig ? backups.testProviderConfig.bind(null, backupConfig) : (next) => next(); - - testBackupConfig(function (error) { + const task = { + args: { + restoreConfig, + oldManifest: app.manifest, + overwriteDns: true + }, + values: {} + }; + addTask(appId, exports.ISTATE_PENDING_RESTORE, task, function (error, result) { if (error) return callback(error); - const restoreConfig = { backupId, backupFormat, backupConfig }; + eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId, fromManifest: app.manifest, toManifest: app.manifest, taskId: result.taskId }); - const task = { - args: { - restoreConfig, - oldManifest: app.manifest, - overwriteDns: true - }, - values: {} - }; - addTask(appId, exports.ISTATE_PENDING_RESTORE, task, function (error, result) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId, fromManifest: app.manifest, toManifest: app.manifest, taskId: result.taskId }); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); }); } @@ -1555,87 +1492,82 @@ function purchaseApp(data, callback) { }); } -function clone(appId, data, user, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function clone(app, data, user, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof data, 'object'); assert(user && typeof user === 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - debug('Will clone app with id:%s', appId); - var location = data.location.toLowerCase(), domain = data.domain.toLowerCase(), portBindings = data.portBindings || null, backupId = data.backupId, - overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false; + overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false, + appId = app.id; assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof portBindings, 'object'); - get(appId, function (error, app) { + backups.get(backupId, function (error, backupInfo) { if (error) return callback(error); - backups.get(backupId, function (error, backupInfo) { + if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config')); + + const manifest = backupInfo.manifest, appStoreId = app.appStoreId; + + // re-validate because this new box version may not accept old configs + error = checkManifestConstraints(manifest); + if (error) return callback(error); + + error = validatePortBindings(portBindings, manifest); + if (error) return callback(error); + + const mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, manifest) : app.mailboxName; + const locations = [{subdomain: location, domain}]; + validateLocations(locations, function (error, domainObjectMap) { if (error) return callback(error); - if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config')); + var newAppId = uuid.v4(); - const manifest = backupInfo.manifest, appStoreId = app.appStoreId; + var data = { + installationState: exports.ISTATE_PENDING_CLONE, + runState: exports.RSTATE_RUNNING, + memoryLimit: app.memoryLimit, + accessRestriction: app.accessRestriction, + sso: !!app.sso, + mailboxName: mailboxName, + mailboxDomain: domain, + enableBackup: app.enableBackup, + reverseProxyConfig: app.reverseProxyConfig, + env: app.env, + alternateDomains: [] + }; - // re-validate because this new box version may not accept old configs - error = checkManifestConstraints(manifest); - if (error) return callback(error); - - error = validatePortBindings(portBindings, manifest); - if (error) return callback(error); - - const mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, manifest) : app.mailboxName; - const locations = [{subdomain: location, domain}]; - validateLocations(locations, function (error, domainObjectMap) { + appdb.add(newAppId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) { + if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, locations, domainObjectMap, portBindings)); if (error) return callback(error); - var newAppId = uuid.v4(); - - var data = { - installationState: exports.ISTATE_PENDING_CLONE, - runState: exports.RSTATE_RUNNING, - memoryLimit: app.memoryLimit, - accessRestriction: app.accessRestriction, - sso: !!app.sso, - mailboxName: mailboxName, - mailboxDomain: domain, - enableBackup: app.enableBackup, - reverseProxyConfig: app.reverseProxyConfig, - env: app.env, - alternateDomains: [] - }; - - appdb.add(newAppId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) { - if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, locations, domainObjectMap, portBindings)); + purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id || 'customapp' }, function (error) { if (error) return callback(error); - purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id || 'customapp' }, function (error) { + const restoreConfig = { backupId: backupId, backupFormat: backupInfo.format }; + const task = { + args: { restoreConfig, overwriteDns, oldManifest: null }, + values: {}, + requiredState: exports.ISTATE_PENDING_CLONE + }; + addTask(newAppId, exports.ISTATE_PENDING_CLONE, task, function (error, result) { if (error) return callback(error); - const restoreConfig = { backupId: backupId, backupFormat: backupInfo.format }; - const task = { - args: { restoreConfig, overwriteDns, oldManifest: null }, - values: {}, - requiredState: exports.ISTATE_PENDING_CLONE - }; - addTask(newAppId, exports.ISTATE_PENDING_CLONE, task, function (error, result) { - if (error) return callback(error); + const newApp = _.extend({}, data, { appStoreId, manifest, location, domain, portBindings }); + newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]); + newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: newApp, taskId: result.taskId }); - const newApp = _.extend({}, data, { appStoreId, manifest, location, domain, portBindings }); - newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]); - newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: newApp, taskId: result.taskId }); - - callback(null, { id: newAppId, taskId: result.taskId }); - }); + callback(null, { id: newAppId, taskId: result.taskId }); }); }); }); @@ -1643,116 +1575,96 @@ function clone(appId, data, user, auditSource, callback) { }); } -function uninstall(appId, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function uninstall(app, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - debug(`Will uninstall app with id : ${appId}`); + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_UNINSTALL); + if (error) return callback(error); - get(appId, function (error, app) { - if (error) return callback(error); - - error = checkAppState(app, exports.ISTATE_PENDING_UNINSTALL); - if (error) return callback(error); - - appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id || 'customapp' }, function (error) { - if (error) return callback(error); - - const task = { - args: {}, - values: {}, - requiredState: null // can run in any state, as long as no task is active - }; - addTask(appId, exports.ISTATE_PENDING_UNINSTALL, task, function (error, result) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId, app, taskId: result.taskId }); - - callback(null, { taskId: result.taskId }); - }); - }); - }); -} - -function start(appId, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - debug('Will start app with id:%s', appId); - - get(appId, function (error, app) { - if (error) return callback(error); - - error = checkAppState(app, exports.ISTATE_PENDING_START); + appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id || 'customapp' }, function (error) { if (error) return callback(error); const task = { args: {}, - values: { runState: exports.RSTATE_RUNNING } + values: {}, + requiredState: null // can run in any state, as long as no task is active }; - addTask(appId, exports.ISTATE_PENDING_START, task, function (error, result) { + addTask(appId, exports.ISTATE_PENDING_UNINSTALL, task, function (error, result) { if (error) return callback(error); - eventlog.add(eventlog.ACTION_APP_START, auditSource, { appId, app, taskId: result.taskId }); + eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId, app, taskId: result.taskId }); callback(null, { taskId: result.taskId }); }); }); } -function stop(appId, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function start(app, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - debug('Will stop app with id:%s', appId); + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_START); + if (error) return callback(error); - get(appId, function (error, app) { + const task = { + args: {}, + values: { runState: exports.RSTATE_RUNNING } + }; + addTask(appId, exports.ISTATE_PENDING_START, task, function (error, result) { if (error) return callback(error); - error = checkAppState(app, exports.ISTATE_PENDING_STOP); - if (error) return callback(error); + eventlog.add(eventlog.ACTION_APP_START, auditSource, { appId, app, taskId: result.taskId }); - const task = { - args: {}, - values: { runState: exports.RSTATE_STOPPED } - }; - addTask(appId, exports.ISTATE_PENDING_STOP, task, function (error, result) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_STOP, auditSource, { appId, app, taskId: result.taskId }); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); } -function restart(appId, auditSource, callback) { - assert.strictEqual(typeof appId, 'string'); +function stop(app, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - debug('Will restart app with id:%s', appId); + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_STOP); + if (error) return callback(error); - get(appId, function (error, app) { + const task = { + args: {}, + values: { runState: exports.RSTATE_STOPPED } + }; + addTask(appId, exports.ISTATE_PENDING_STOP, task, function (error, result) { if (error) return callback(error); - error = checkAppState(app, exports.ISTATE_PENDING_RESTART); + eventlog.add(eventlog.ACTION_APP_STOP, auditSource, { appId, app, taskId: result.taskId }); + + callback(null, { taskId: result.taskId }); + }); +} + +function restart(app, auditSource, callback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof auditSource, 'object'); + assert.strictEqual(typeof callback, 'function'); + + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_RESTART); + if (error) return callback(error); + + const task = { + args: {}, + values: { runState: exports.RSTATE_RUNNING } + }; + addTask(appId, exports.ISTATE_PENDING_RESTART, task, function (error, result) { if (error) return callback(error); - const task = { - args: {}, - values: { runState: exports.RSTATE_RUNNING } - }; - addTask(appId, exports.ISTATE_PENDING_RESTART, task, function (error, result) { - if (error) return callback(error); + eventlog.add(eventlog.ACTION_APP_RESTART, auditSource, { appId, app, taskId: result.taskId }); - eventlog.add(eventlog.ACTION_APP_RESTART, auditSource, { appId, app, taskId: result.taskId }); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); } @@ -1774,54 +1686,50 @@ function checkManifestConstraints(manifest) { return null; } -function exec(appId, options, callback) { - assert.strictEqual(typeof appId, 'string'); +function exec(app, options, callback) { + assert.strictEqual(typeof app, 'object'); assert(options && typeof options === 'object'); assert.strictEqual(typeof callback, 'function'); var cmd = options.cmd || [ '/bin/bash' ]; assert(util.isArray(cmd) && cmd.length > 0); - get(appId, function (error, app) { + if (app.installationState !== exports.ISTATE_INSTALLED || app.runState !== exports.RSTATE_RUNNING) { + return callback(new BoxError(BoxError.BAD_STATE, 'App not installed or running')); + } + + var execOptions = { + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + // A pseudo tty is a terminal which processes can detect (for example, disable colored output) + // Creating a pseudo terminal also assigns a terminal driver which detects control sequences + // When passing binary data, tty must be disabled. In addition, the stdout/stderr becomes a single + // unified stream because of the nature of a tty (see https://github.com/docker/docker/issues/19696) + Tty: options.tty, + Cmd: cmd + }; + + var startOptions = { + Detach: false, + Tty: options.tty, + // hijacking upgrades the docker connection from http to tcp. because of this upgrade, + // we can work with half-close connections (not defined in http). this way, the client + // can properly signal that stdin is EOF by closing it's side of the socket. In http, + // the whole connection will be dropped when stdin get EOF. + // https://github.com/apocas/dockerode/commit/b4ae8a03707fad5de893f302e4972c1e758592fe + hijack: true, + stream: true, + stdin: true, + stdout: true, + stderr: true + }; + + docker.execContainer(app.containerId, { execOptions, startOptions, rows: options.rows, columns: options.columns }, function (error, stream) { + if (error && error.statusCode === 409) return callback(new BoxError(BoxError.BAD_STATE, error.message)); // container restarting/not running if (error) return callback(error); - if (app.installationState !== exports.ISTATE_INSTALLED || app.runState !== exports.RSTATE_RUNNING) { - return callback(new BoxError(BoxError.BAD_STATE, 'App not installed or running')); - } - - var execOptions = { - AttachStdin: true, - AttachStdout: true, - AttachStderr: true, - // A pseudo tty is a terminal which processes can detect (for example, disable colored output) - // Creating a pseudo terminal also assigns a terminal driver which detects control sequences - // When passing binary data, tty must be disabled. In addition, the stdout/stderr becomes a single - // unified stream because of the nature of a tty (see https://github.com/docker/docker/issues/19696) - Tty: options.tty, - Cmd: cmd - }; - - var startOptions = { - Detach: false, - Tty: options.tty, - // hijacking upgrades the docker connection from http to tcp. because of this upgrade, - // we can work with half-close connections (not defined in http). this way, the client - // can properly signal that stdin is EOF by closing it's side of the socket. In http, - // the whole connection will be dropped when stdin get EOF. - // https://github.com/apocas/dockerode/commit/b4ae8a03707fad5de893f302e4972c1e758592fe - hijack: true, - stream: true, - stdin: true, - stdout: true, - stderr: true - }; - - docker.execContainer(app.containerId, { execOptions, startOptions, rows: options.rows, columns: options.columns }, function (error, stream) { - if (error && error.statusCode === 409) return callback(new BoxError(BoxError.BAD_STATE, error.message)); // container restarting/not running - if (error) return callback(error); - - callback(null, stream); - }); + callback(null, stream); }); } @@ -1876,44 +1784,36 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is { }, callback); } -function backup(appId, callback) { - assert.strictEqual(typeof appId, 'string'); +function backup(app, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); - get(appId, function (error, app) { + const appId = app.id; + + let error = checkAppState(app, exports.ISTATE_PENDING_BACKUP); + if (error) return callback(error); + + const task = { + args: {}, + values: {} + }; + addTask(appId, exports.ISTATE_PENDING_BACKUP, task, (error, result) => { if (error) return callback(error); - error = checkAppState(app, exports.ISTATE_PENDING_BACKUP); - if (error) return callback(error); - - const task = { - args: {}, - values: {} - }; - addTask(appId, exports.ISTATE_PENDING_BACKUP, task, (error, result) => { - if (error) return callback(error); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); } -function listBackups(page, perPage, appId, callback) { +function listBackups(app, page, perPage, callback) { + assert.strictEqual(typeof app, 'object'); assert(typeof page === 'number' && page > 0); assert(typeof perPage === 'number' && perPage > 0); - - assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof callback, 'function'); - appdb.exists(appId, function (error, exists) { + backups.getByAppIdPaged(page, perPage, app.id, function (error, results) { if (error) return callback(error); - if (!exists) return callback(new BoxError(BoxError.NOT_FOUND)); - backups.getByAppIdPaged(page, perPage, appId, function (error, results) { - if (error) return callback(error); - - callback(null, results); - }); + callback(null, results); }); } @@ -2009,13 +1909,12 @@ function schedulePendingTasks(callback) { }); } -function downloadFile(appId, filePath, callback) { - assert.strictEqual(typeof appId, 'string'); +function downloadFile(app, filePath, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof filePath, 'string'); assert.strictEqual(typeof callback, 'function'); - debug(`downloadFile: ${filePath}`); // no need to escape filePath because we don't rely on bash - exec(appId, { cmd: [ 'stat', '--printf=%F-%s', filePath ], tty: true }, function (error, stream) { + exec(app, { cmd: [ 'stat', '--printf=%F-%s', filePath ], tty: true }, function (error, stream) { if (error) return callback(error); var data = ''; @@ -2040,7 +1939,7 @@ function downloadFile(appId, filePath, callback) { return callback(new BoxError(BoxError.NOT_FOUND, 'only files or dirs can be downloaded')); } - exec(appId, { cmd: cmd , tty: false }, function (error, stream) { + exec(app, { cmd: cmd , tty: false }, function (error, stream) { if (error) return callback(error); var stdoutStream = new TransformStream({ @@ -2068,14 +1967,14 @@ function downloadFile(appId, filePath, callback) { stream.pipe(stdoutStream); - return callback(null, stdoutStream, { filename: filename, size: size }); + callback(null, stdoutStream, { filename: filename, size: size }); }); }); }); } -function uploadFile(appId, sourceFilePath, destFilePath, callback) { - assert.strictEqual(typeof appId, 'string'); +function uploadFile(app, sourceFilePath, destFilePath, callback) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof sourceFilePath, 'string'); assert.strictEqual(typeof destFilePath, 'string'); assert.strictEqual(typeof callback, 'function'); @@ -2091,7 +1990,7 @@ function uploadFile(appId, sourceFilePath, destFilePath, callback) { const escapedDestFilePath = safe.child_process.execSync(`printf %q '${destFilePath.replace(/'/g, '\'\\\'\'')}'`, { shell: '/bin/bash', encoding: 'utf8' }); debug(`uploadFile: ${sourceFilePath} -> ${escapedDestFilePath}`); - exec(appId, { cmd: [ 'bash', '-c', `cat - > ${escapedDestFilePath}` ], tty: false }, function (error, stream) { + exec(app, { cmd: [ 'bash', '-c', `cat - > ${escapedDestFilePath}` ], tty: false }, function (error, stream) { if (error) return done(error); var readFile = fs.createReadStream(sourceFilePath); diff --git a/src/routes/apps.js b/src/routes/apps.js index ffc2f7b0d..5223c3e0d 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -4,16 +4,16 @@ exports = module.exports = { getApp: getApp, getApps: getApps, getAppIcon: getAppIcon, - installApp: installApp, - uninstallApp: uninstallApp, - restoreApp: restoreApp, + install: install, + uninstall: uninstall, + restore: restore, importApp: importApp, - backupApp: backupApp, - updateApp: updateApp, + backup: backup, + update: update, getLogs: getLogs, getLogStream: getLogStream, listBackups: listBackups, - repairApp: repairApp, + repair: repair, setAccessRestriction: setAccessRestriction, setLabel: setLabel, @@ -31,16 +31,18 @@ exports = module.exports = { setLocation: setLocation, setDataDir: setDataDir, - stopApp: stopApp, - startApp: startApp, - restartApp: restartApp, + stop: stop, + start: start, + restart: restart, exec: exec, execWebSocket: execWebSocket, - cloneApp: cloneApp, + clone: clone, uploadFile: uploadFile, - downloadFile: downloadFile + downloadFile: downloadFile, + + load: load }; var apps = require('../apps.js'), @@ -55,16 +57,24 @@ var apps = require('../apps.js'), util = require('util'), WebSocket = require('ws'); -function getApp(req, res, next) { +function load(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); - apps.get(req.params.id, function (error, app) { + apps.get(req.params.id, function (error, result) { if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, apps.removeInternalFields(app))); + req.resource = result; + + next(); }); } +function getApp(req, res, next) { + assert.strictEqual(typeof req.resource, 'object'); + + next(new HttpSuccess(200, apps.removeInternalFields(req.resource))); +} + function getApps(req, res, next) { assert.strictEqual(typeof req.user, 'object'); @@ -78,16 +88,16 @@ function getApps(req, res, next) { } function getAppIcon(req, res, next) { - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); - apps.getIconPath(req.params.id, { original: req.query.original }, function (error, iconPath) { + apps.getIconPath(req.resource, { original: req.query.original }, function (error, iconPath) { if (error) return next(BoxError.toHttpError(error)); res.sendFile(iconPath); }); } -function installApp(req, res, next) { +function install(req, res, next) { assert.strictEqual(typeof req.body, 'object'); const data = req.body; @@ -151,11 +161,11 @@ function installApp(req, res, next) { function setAccessRestriction(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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) { + apps.setAccessRestriction(req.resource, req.body.accessRestriction, auditSource.fromRequest(req), function (error) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -164,11 +174,11 @@ function setAccessRestriction(req, res, next) { function setLabel(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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) { + apps.setLabel(req.resource, req.body.label, auditSource.fromRequest(req), function (error) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -177,12 +187,12 @@ function setLabel(req, res, next) { function setTags(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, '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')); - apps.setTags(req.params.id, req.body.tags, auditSource.fromRequest(req), function (error) { + apps.setTags(req.resource, req.body.tags, auditSource.fromRequest(req), function (error) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -191,11 +201,11 @@ function setTags(req, res, next) { function setIcon(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, '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')); - apps.setIcon(req.params.id, req.body.icon, auditSource.fromRequest(req), function (error) { + apps.setIcon(req.resource, req.body.icon, auditSource.fromRequest(req), function (error) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -204,11 +214,11 @@ function setIcon(req, res, next) { function setMemoryLimit(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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) { + apps.setMemoryLimit(req.resource, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); @@ -217,11 +227,11 @@ function setMemoryLimit(req, res, next) { function setCpuShares(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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) { + apps.setCpuShares(req.resource, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); @@ -230,11 +240,11 @@ function setCpuShares(req, res, next) { function setAutomaticBackup(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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) { + apps.setAutomaticBackup(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -243,11 +253,11 @@ function setAutomaticBackup(req, res, next) { function setAutomaticUpdate(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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) { + apps.setAutomaticUpdate(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -256,13 +266,13 @@ function setAutomaticUpdate(req, res, next) { function setReverseProxyConfig(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, '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')); - apps.setReverseProxyConfig(req.params.id, req.body, auditSource.fromRequest(req), function (error) { + apps.setReverseProxyConfig(req.resource, req.body, auditSource.fromRequest(req), function (error) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -271,14 +281,14 @@ function setReverseProxyConfig(req, res, next) { function setCertificate(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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) { + apps.setCertificate(req.resource, req.body, auditSource.fromRequest(req), function (error) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -287,12 +297,12 @@ function setCertificate(req, res, next) { function setEnvironment(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, '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')); - apps.setEnvironment(req.params.id, req.body.env, auditSource.fromRequest(req), function (error, result) { + apps.setEnvironment(req.resource, req.body.env, auditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); @@ -301,11 +311,11 @@ function setEnvironment(req, res, next) { function setDebugMode(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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) { + apps.setDebugMode(req.resource, req.body.debugMode, auditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); @@ -314,12 +324,12 @@ function setDebugMode(req, res, next) { function setMailbox(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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) { + apps.setMailbox(req.resource, 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 })); @@ -328,7 +338,7 @@ function setMailbox(req, res, next) { function setLocation(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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')); @@ -343,7 +353,7 @@ function setLocation(req, res, next) { 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) { + apps.setLocation(req.resource, req.body, auditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); @@ -352,20 +362,20 @@ function setLocation(req, res, next) { function setDataDir(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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) { + apps.setDataDir(req.resource, 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) { +function repair(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); const data = req.body; @@ -379,22 +389,22 @@ function repairApp(req, res, next) { 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) { + apps.repair(req.resource, data, auditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } -function restoreApp(req, res, next) { +function restore(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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) { + apps.restore(req.resource, data.backupId, auditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); @@ -403,7 +413,7 @@ function restoreApp(req, res, next) { function importApp(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); var data = req.body; @@ -425,16 +435,16 @@ function importApp(req, res, next) { } } - apps.importApp(req.params.id, data, auditSource.fromRequest(req), function (error, result) { + apps.importApp(req.resource, data, auditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } -function cloneApp(req, res, next) { +function clone(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); var data = req.body; @@ -445,66 +455,66 @@ function cloneApp(req, res, next) { 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) { + apps.clone(req.resource, 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'); +function backup(req, res, next) { + assert.strictEqual(typeof req.resource, 'object'); - apps.backup(req.params.id, function (error, result) { + apps.backup(req.resource, 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'); +function uninstall(req, res, next) { + assert.strictEqual(typeof req.resource, 'object'); - apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) { + apps.uninstall(req.resource, 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'); +function start(req, res, next) { + assert.strictEqual(typeof req.resource, 'object'); - apps.start(req.params.id, auditSource.fromRequest(req), function (error, result) { + apps.start(req.resource, 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'); +function stop(req, res, next) { + assert.strictEqual(typeof req.resource, 'object'); - apps.stop(req.params.id, auditSource.fromRequest(req), function (error, result) { + apps.stop(req.resource, 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'); +function restart(req, res, next) { + assert.strictEqual(typeof req.resource, 'object'); - apps.restart(req.params.id, auditSource.fromRequest(req), function (error, result) { + apps.restart(req.resource, 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'); +function update(req, res, next) { assert.strictEqual(typeof req.body, 'object'); + assert.strictEqual(typeof req.resource, 'object'); var data = req.body; @@ -523,7 +533,7 @@ function updateApp(req, res, next) { data.appStoreId = appStoreId; data.manifest = manifest; - apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) { + apps.update(req.resource, req.body, auditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); @@ -533,7 +543,7 @@ function updateApp(req, res, next) { // this route is for streaming logs function getLogStream(req, res, next) { - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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')); @@ -548,7 +558,7 @@ function getLogStream(req, res, next) { format: 'json' }; - apps.getLogs(req.params.id, options, function (error, logStream) { + apps.getLogs(req.resource, options, function (error, logStream) { if (error) return next(BoxError.toHttpError(error)); res.writeHead(200, { @@ -570,7 +580,7 @@ function getLogStream(req, res, next) { } function getLogs(req, res, next) { - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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')); @@ -581,7 +591,7 @@ function getLogs(req, res, next) { format: req.query.format || 'json' }; - apps.getLogs(req.params.id, options, function (error, logStream) { + apps.getLogs(req.resource, options, function (error, logStream) { if (error) return next(BoxError.toHttpError(error)); res.writeHead(200, { @@ -617,7 +627,7 @@ function demuxStream(stream, stdin) { } function exec(req, res, next) { - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); var cmd = null; if (req.query.cmd) { @@ -633,7 +643,7 @@ function exec(req, res, next) { var tty = req.query.tty === 'true' ? true : false; - apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) { + apps.exec(req.resource, { 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')); @@ -655,7 +665,7 @@ function exec(req, res, next) { } function execWebSocket(req, res, next) { - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); var cmd = null; if (req.query.cmd) { @@ -671,7 +681,7 @@ function execWebSocket(req, res, next) { var tty = req.query.tty === 'true' ? true : false; - apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) { + apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) { if (error) return next(BoxError.toHttpError(error)); req.clearTimeout(); @@ -701,7 +711,7 @@ function execWebSocket(req, res, next) { } function listBackups(req, res, next) { - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); 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')); @@ -709,7 +719,7 @@ function listBackups(req, res, next) { 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) { + apps.listBackups(req.resource, page, perPage, function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, { backups: result })); @@ -717,12 +727,12 @@ function listBackups(req, res, next) { } function uploadFile(req, res, next) { - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided')); if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart')); - apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) { + apps.uploadFile(req.resource, req.files.file.path, req.query.file, function (error) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, {})); @@ -730,11 +740,11 @@ function uploadFile(req, res, next) { } function downloadFile(req, res, next) { - assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.resource, 'object'); if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided')); - apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) { + apps.downloadFile(req.resource, req.query.file, function (error, stream, info) { if (error) return next(BoxError.toHttpError(error)); var headers = { diff --git a/src/server.js b/src/server.js index 39ba631fd..fd3746b81 100644 --- a/src/server.js +++ b/src/server.js @@ -191,45 +191,45 @@ function initializeExpressSync() { // app routes router.get ('/api/v1/apps', token, routes.apps.getApps); - router.get ('/api/v1/apps/:id', token, authorizeAdmin, routes.apps.getApp); - router.get ('/api/v1/apps/:id/icon', token, routes.apps.getAppIcon); + router.get ('/api/v1/apps/:id', token, authorizeAdmin, routes.apps.load, routes.apps.getApp); + router.get ('/api/v1/apps/:id/icon', token, routes.apps.load, routes.apps.getAppIcon); - router.post('/api/v1/apps/install', token, authorizeAdmin, routes.apps.installApp); - router.post('/api/v1/apps/:id/uninstall', token, authorizeAdmin, routes.apps.uninstallApp); + router.post('/api/v1/apps/install', token, authorizeAdmin, routes.apps.install); + router.post('/api/v1/apps/:id/uninstall', token, authorizeAdmin, routes.apps.load, routes.apps.uninstall); - router.post('/api/v1/apps/:id/configure/access_restriction', token, authorizeAdmin, routes.apps.setAccessRestriction); - router.post('/api/v1/apps/:id/configure/label', token, authorizeAdmin, routes.apps.setLabel); - router.post('/api/v1/apps/:id/configure/tags', token, authorizeAdmin, routes.apps.setTags); - router.post('/api/v1/apps/:id/configure/icon', token, authorizeAdmin, routes.apps.setIcon); - router.post('/api/v1/apps/:id/configure/memory_limit', token, authorizeAdmin, routes.apps.setMemoryLimit); - router.post('/api/v1/apps/:id/configure/cpu_shares', token, authorizeAdmin, routes.apps.setCpuShares); - router.post('/api/v1/apps/:id/configure/automatic_backup', token, authorizeAdmin, routes.apps.setAutomaticBackup); - router.post('/api/v1/apps/:id/configure/automatic_update', token, authorizeAdmin, routes.apps.setAutomaticUpdate); - router.post('/api/v1/apps/:id/configure/reverse_proxy', token, authorizeAdmin, routes.apps.setReverseProxyConfig); - router.post('/api/v1/apps/:id/configure/cert', token, authorizeAdmin, routes.apps.setCertificate); - router.post('/api/v1/apps/:id/configure/debug_mode', token, authorizeAdmin, routes.apps.setDebugMode); - router.post('/api/v1/apps/:id/configure/mailbox', token, authorizeAdmin, routes.apps.setMailbox); - router.post('/api/v1/apps/:id/configure/env', token, authorizeAdmin, routes.apps.setEnvironment); - router.post('/api/v1/apps/:id/configure/data_dir', token, authorizeAdmin, routes.apps.setDataDir); - router.post('/api/v1/apps/:id/configure/location', token, authorizeAdmin, routes.apps.setLocation); + router.post('/api/v1/apps/:id/configure/access_restriction', token, authorizeAdmin, routes.apps.load, routes.apps.setAccessRestriction); + router.post('/api/v1/apps/:id/configure/label', token, authorizeAdmin, routes.apps.load, routes.apps.setLabel); + router.post('/api/v1/apps/:id/configure/tags', token, authorizeAdmin, routes.apps.load, routes.apps.setTags); + router.post('/api/v1/apps/:id/configure/icon', token, authorizeAdmin, routes.apps.load, routes.apps.setIcon); + router.post('/api/v1/apps/:id/configure/memory_limit', token, authorizeAdmin, routes.apps.load, routes.apps.setMemoryLimit); + router.post('/api/v1/apps/:id/configure/cpu_shares', token, authorizeAdmin, routes.apps.load, routes.apps.setCpuShares); + router.post('/api/v1/apps/:id/configure/automatic_backup', token, authorizeAdmin, routes.apps.load, routes.apps.setAutomaticBackup); + router.post('/api/v1/apps/:id/configure/automatic_update', token, authorizeAdmin, routes.apps.load, routes.apps.setAutomaticUpdate); + router.post('/api/v1/apps/:id/configure/reverse_proxy', token, authorizeAdmin, routes.apps.load, routes.apps.setReverseProxyConfig); + router.post('/api/v1/apps/:id/configure/cert', token, authorizeAdmin, routes.apps.load, routes.apps.setCertificate); + router.post('/api/v1/apps/:id/configure/debug_mode', token, authorizeAdmin, routes.apps.load, routes.apps.setDebugMode); + router.post('/api/v1/apps/:id/configure/mailbox', token, authorizeAdmin, routes.apps.load, routes.apps.setMailbox); + router.post('/api/v1/apps/:id/configure/env', token, authorizeAdmin, routes.apps.load, routes.apps.setEnvironment); + router.post('/api/v1/apps/:id/configure/data_dir', token, authorizeAdmin, routes.apps.load, routes.apps.setDataDir); + router.post('/api/v1/apps/:id/configure/location', token, authorizeAdmin, routes.apps.load, routes.apps.setLocation); - router.post('/api/v1/apps/:id/repair', token, authorizeAdmin, routes.apps.repairApp); - router.post('/api/v1/apps/:id/update', token, authorizeAdmin, routes.apps.updateApp); - router.post('/api/v1/apps/:id/restore', token, authorizeAdmin, routes.apps.restoreApp); - router.post('/api/v1/apps/:id/import', token, authorizeAdmin, routes.apps.importApp); - router.post('/api/v1/apps/:id/backup', token, authorizeAdmin, routes.apps.backupApp); - router.get ('/api/v1/apps/:id/backups', token, authorizeAdmin, routes.apps.listBackups); - router.post('/api/v1/apps/:id/stop', token, authorizeAdmin, routes.apps.stopApp); - router.post('/api/v1/apps/:id/start', token, authorizeAdmin, routes.apps.startApp); - router.post('/api/v1/apps/:id/restart', token, authorizeAdmin, routes.apps.restartApp); - router.get ('/api/v1/apps/:id/logstream', token, authorizeAdmin, routes.apps.getLogStream); - router.get ('/api/v1/apps/:id/logs', token, authorizeAdmin, routes.apps.getLogs); - router.get ('/api/v1/apps/:id/exec', token, authorizeAdmin, routes.apps.exec); + router.post('/api/v1/apps/:id/repair', token, authorizeAdmin, routes.apps.load, routes.apps.repair); + router.post('/api/v1/apps/:id/update', token, authorizeAdmin, routes.apps.load, routes.apps.update); + router.post('/api/v1/apps/:id/restore', token, authorizeAdmin, routes.apps.load, routes.apps.restore); + router.post('/api/v1/apps/:id/import', token, authorizeAdmin, routes.apps.load, routes.apps.importApp); + router.post('/api/v1/apps/:id/backup', token, authorizeAdmin, routes.apps.load, routes.apps.backup); + router.get ('/api/v1/apps/:id/backups', token, authorizeAdmin, routes.apps.load, routes.apps.listBackups); + router.post('/api/v1/apps/:id/start', token, authorizeAdmin, routes.apps.load, routes.apps.start); + router.post('/api/v1/apps/:id/stop', token, authorizeAdmin, routes.apps.load, routes.apps.stop); + router.post('/api/v1/apps/:id/restart', token, authorizeAdmin, routes.apps.load, routes.apps.restart); + router.get ('/api/v1/apps/:id/logstream', token, authorizeAdmin, routes.apps.load, routes.apps.getLogStream); + router.get ('/api/v1/apps/:id/logs', token, authorizeAdmin, routes.apps.load, routes.apps.getLogs); + router.get ('/api/v1/apps/:id/exec', token, authorizeAdmin, routes.apps.load, routes.apps.exec); // websocket cannot do bearer authentication - router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, users.ROLE_ADMIN), routes.apps.execWebSocket); - router.post('/api/v1/apps/:id/clone', token, authorizeAdmin, routes.apps.cloneApp); - router.get ('/api/v1/apps/:id/download', token, authorizeAdmin, routes.apps.downloadFile); - router.post('/api/v1/apps/:id/upload', token, authorizeAdmin, multipart, routes.apps.uploadFile); + router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, users.ROLE_ADMIN), routes.apps.load, routes.apps.execWebSocket); + router.post('/api/v1/apps/:id/clone', token, authorizeAdmin, routes.apps.load, routes.apps.clone); + router.get ('/api/v1/apps/:id/download', token, authorizeAdmin, routes.apps.load, routes.apps.downloadFile); + router.post('/api/v1/apps/:id/upload', token, authorizeAdmin, multipart, routes.apps.load, routes.apps.uploadFile); router.get ('/api/v1/branding/:setting', token, authorizeOwner, routes.branding.get); router.post('/api/v1/branding/:setting', token, authorizeOwner, (req, res, next) => {