diff --git a/src/appdb.js b/src/appdb.js deleted file mode 100644 index ce7c97906..000000000 --- a/src/appdb.js +++ /dev/null @@ -1,468 +0,0 @@ -'use strict'; - -exports = module.exports = { - get, - add, - del, - update, - getAll, - getPortBindings, - delPortBinding, - - getByIpAddress, - - getIcons, - - setHealth, - setTask, - - // subdomain table types - SUBDOMAIN_TYPE_PRIMARY: 'primary', - SUBDOMAIN_TYPE_REDIRECT: 'redirect', - SUBDOMAIN_TYPE_ALIAS: 'alias', - - _clear: clear -}; - -const assert = require('assert'), - async = require('async'), - BoxError = require('./boxerror.js'), - database = require('./database.js'), - safe = require('safetydance'), - util = require('util'); - -const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState', - 'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares', - 'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', - 'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp', - 'apps.creationTime', 'apps.updateTime', 'apps.enableMailbox', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate', - 'apps.dataDir', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(','); - -const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(','); - -function postProcess(result) { - assert.strictEqual(typeof result, 'object'); - - assert(result.manifestJson === null || typeof result.manifestJson === 'string'); - result.manifest = safe.JSON.parse(result.manifestJson); - delete result.manifestJson; - - assert(result.tagsJson === null || typeof result.tagsJson === 'string'); - result.tags = safe.JSON.parse(result.tagsJson) || []; - delete result.tagsJson; - - assert(result.reverseProxyConfigJson === null || typeof result.reverseProxyConfigJson === 'string'); - result.reverseProxyConfig = safe.JSON.parse(result.reverseProxyConfigJson) || {}; - delete result.reverseProxyConfigJson; - - assert(result.hostPorts === null || typeof result.hostPorts === 'string'); - assert(result.environmentVariables === null || typeof result.environmentVariables === 'string'); - - result.portBindings = { }; - let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(','); - let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(','); - let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(','); - - delete result.hostPorts; - delete result.environmentVariables; - delete result.portTypes; - - for (let i = 0; i < environmentVariables.length; i++) { - result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] }; - } - - assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string'); - result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson); - if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = []; - delete result.accessRestrictionJson; - - result.sso = !!result.sso; - result.enableBackup = !!result.enableBackup; - result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; - result.enableMailbox = !!result.enableMailbox; - result.proxyAuth = !!result.proxyAuth; - result.hasIcon = !!result.hasIcon; - result.hasAppStoreIcon = !!result.hasAppStoreIcon; - - assert(result.debugModeJson === null || typeof result.debugModeJson === 'string'); - result.debugMode = safe.JSON.parse(result.debugModeJson); - delete result.debugModeJson; - - assert(result.servicesConfigJson === null || typeof result.servicesConfigJson === 'string'); - result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {}; - delete result.servicesConfigJson; - - let subdomains = JSON.parse(result.subdomains), domains = JSON.parse(result.domains), subdomainTypes = JSON.parse(result.subdomainTypes); - delete result.subdomains; - delete result.domains; - delete result.subdomainTypes; - - result.alternateDomains = []; - result.aliasDomains = []; - for (let i = 0; i < subdomainTypes.length; i++) { - if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_PRIMARY) { - result.location = subdomains[i]; - result.domain = domains[i]; - } else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_REDIRECT) { - result.alternateDomains.push({ domain: domains[i], subdomain: subdomains[i] }); - } else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_ALIAS) { - result.aliasDomains.push({ domain: domains[i], subdomain: subdomains[i] }); - } - } - - let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues); - delete result.envNames; - delete result.envValues; - result.env = {}; - for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty - if (envNames[i]) result.env[envNames[i]] = envValues[i]; - } - - let volumeIds = JSON.parse(result.volumeIds); - delete result.volumeIds; - let volumeReadOnlys = JSON.parse(result.volumeReadOnlys); - delete result.volumeReadOnlys; - - result.mounts = volumeIds[0] === null ? [] : volumeIds.map((v, idx) => { return { volumeId: v, readOnly: !!volumeReadOnlys[idx] }; }); // NOTE: volumeIds is [null] when volumes of an app is empty - - result.error = safe.JSON.parse(result.errorJson); - delete result.errorJson; - - result.taskId = result.taskId ? String(result.taskId) : null; -} - -// each query simply join apps table with another table by id. we then join the full result together -const PB_QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id'; -const ENV_QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id'; -const SUBDOMAIN_QUERY = 'SELECT id, JSON_ARRAYAGG(subdomains.subdomain) AS subdomains, JSON_ARRAYAGG(subdomains.domain) AS domains, JSON_ARRAYAGG(subdomains.type) AS subdomainTypes FROM apps LEFT JOIN subdomains ON apps.id = subdomains.appId GROUP BY apps.id'; -const MOUNTS_QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id'; -const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, subdomains, domains, subdomainTypes, volumeIds, volumeReadOnlys FROM apps` - + ` LEFT JOIN (${PB_QUERY}) AS q1 on q1.id = apps.id` - + ` LEFT JOIN (${ENV_QUERY}) AS q2 on q2.id = apps.id` - + ` LEFT JOIN (${SUBDOMAIN_QUERY}) AS q3 on q3.id = apps.id` - + ` LEFT JOIN (${MOUNTS_QUERY}) AS q4 on q4.id = apps.id`; - -function get(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query(`${APPS_QUERY} WHERE apps.id = ?`, [ id ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found')); - - postProcess(result[0]); - - callback(null, result[0]); - }); -} - -function getByIpAddress(ip, callback) { - assert.strictEqual(typeof ip, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query(`${APPS_QUERY} WHERE apps.containerIp = ?`, [ ip ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found')); - - postProcess(result[0]); - - callback(null, result[0]); - }); -} - -function getAll(callback) { - assert.strictEqual(typeof callback, 'function'); - - database.query(`${APPS_QUERY} ORDER BY apps.id`, [ ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - results.forEach(postProcess); - - callback(null, results); - }); -} - -function add(id, appStoreId, manifest, location, domain, portBindings, data, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof appStoreId, 'string'); - assert(manifest && typeof manifest === 'object'); - assert.strictEqual(typeof manifest.version, 'string'); - assert.strictEqual(typeof location, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof portBindings, 'object'); - assert(data && typeof data === 'object'); - assert.strictEqual(typeof callback, 'function'); - - portBindings = portBindings || { }; - - var manifestJson = JSON.stringify(manifest); - - const accessRestriction = data.accessRestriction || null; - const accessRestrictionJson = JSON.stringify(accessRestriction); - const memoryLimit = data.memoryLimit || 0; - const cpuShares = data.cpuShares || 512; - const installationState = data.installationState; - const runState = data.runState; - const sso = 'sso' in data ? data.sso : null; - const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null; - const env = data.env || {}; - const label = data.label || null; - const tagsJson = data.tags ? JSON.stringify(data.tags) : null; - const mailboxName = data.mailboxName || null; - const mailboxDomain = data.mailboxDomain || null; - const reverseProxyConfigJson = data.reverseProxyConfig ? JSON.stringify(data.reverseProxyConfig) : null; - const servicesConfigJson = data.servicesConfig ? JSON.stringify(data.servicesConfig) : null; - const enableMailbox = data.enableMailbox || false; - const icon = data.icon || null; - - let queries = []; - - queries.push({ - query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, ' - + 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, enableMailbox) ' - + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, - sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, enableMailbox ] - }); - - queries.push({ - query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', - args: [ id, domain, location, exports.SUBDOMAIN_TYPE_PRIMARY ] - }); - - Object.keys(portBindings).forEach(function (env) { - queries.push({ - query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)', - args: [ env, portBindings[env].hostPort, portBindings[env].type, id ] - }); - }); - - Object.keys(env).forEach(function (name) { - queries.push({ - query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)', - args: [ id, name, env[name] ] - }); - }); - - if (data.alternateDomains) { - data.alternateDomains.forEach(function (d) { - queries.push({ - query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', - args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ] - }); - }); - } - - if (data.aliasDomains) { - data.aliasDomains.forEach(function (d) { - queries.push({ - query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', - args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_ALIAS ] - }); - }); - } - - database.transaction(queries, function (error) { - if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message)); - if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'no such domain')); - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function getPortBindings(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + PORT_BINDINGS_FIELDS + ' FROM appPortBindings WHERE appId = ?', [ id ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - var portBindings = { }; - for (let i = 0; i < results.length; i++) { - portBindings[results[i].environmentVariable] = { hostPort: results[i].hostPort, type: results[i].type }; - } - - callback(null, portBindings); - }); -} - -function getIcons(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT icon, appStoreIcon FROM apps WHERE id = ?', [ id ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, { icon: results[0].icon, appStoreIcon: results[0].appStoreIcon }); - }); -} - -function delPortBinding(hostPort, type, callback) { - assert.strictEqual(typeof hostPort, 'number'); - assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found')); - - callback(null); - }); -} - -function del(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - var queries = [ - { query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] }, - { query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] }, - { query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] }, - { query: 'DELETE FROM appPasswords WHERE identifier = ?', args: [ id ] }, - { query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ] }, - { query: 'DELETE FROM apps WHERE id = ?', args: [ id ] } - ]; - - database.transaction(queries, function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results[5].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found')); - - callback(null); - }); -} - -function clear(callback) { - assert.strictEqual(typeof callback, 'function'); - - async.series([ - database.query.bind(null, 'DELETE FROM subdomains'), - database.query.bind(null, 'DELETE FROM appPortBindings'), - database.query.bind(null, 'DELETE FROM appAddonConfigs'), - database.query.bind(null, 'DELETE FROM appEnvVars'), - database.query.bind(null, 'DELETE FROM apps') - ], function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - return callback(null); - }); -} - -function update(id, app, callback) { - // ts is useful as a versioning mechanism (for example, icon changed). update the timestamp explicity in code instead of db. - // this way health and healthTime can be updated without changing ts - app.ts = new Date(); - updateWithConstraints(id, app, '', callback); -} - -function updateWithConstraints(id, app, constraints, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof constraints, 'string'); - assert.strictEqual(typeof callback, 'function'); - assert(!('portBindings' in app) || typeof app.portBindings === 'object'); - assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === ''); - assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains)); - assert(!('aliasDomains' in app) || Array.isArray(app.aliasDomains)); - assert(!('tags' in app) || Array.isArray(app.tags)); - assert(!('env' in app) || typeof app.env === 'object'); - - var queries = [ ]; - - if ('portBindings' in app) { - var portBindings = app.portBindings || { }; - // replace entries by app id - queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] }); - Object.keys(portBindings).forEach(function (env) { - var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ]; - queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values }); - }); - } - - if ('env' in app) { - queries.push({ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] }); - - Object.keys(app.env).forEach(function (name) { - queries.push({ - query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)', - args: [ id, name, app.env[name] ] - }); - }); - } - - if ('location' in app && 'domain' in app) { // must be updated together as they are unique together - queries.push({ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ]}); // all locations of an app must be updated together - queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.location, exports.SUBDOMAIN_TYPE_PRIMARY ]}); - - if ('alternateDomains' in app) { - app.alternateDomains.forEach(function (d) { - queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]}); - }); - } - - if ('aliasDomains' in app) { - app.aliasDomains.forEach(function (d) { - queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_ALIAS ]}); - }); - } - } - - if ('mounts' in app) { - queries.push({ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ]}); - app.mounts.forEach(function (m) { - queries.push({ query: 'INSERT INTO appMounts (appId, volumeId, readOnly) VALUES (?, ?, ?)', args: [ id, m.volumeId, m.readOnly ]}); - }); - } - - var fields = [ ], values = [ ]; - for (let p in app) { - if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig') { - fields.push(`${p}Json = ?`); - values.push(JSON.stringify(app[p])); - } else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'aliasDomains' && p !== 'env' && p !== 'mounts') { - fields.push(p + ' = ?'); - values.push(app[p]); - } - } - - if (values.length !== 0) { - values.push(id); - queries.push({ query: 'UPDATE apps SET ' + fields.join(', ') + ' WHERE id = ? ' + constraints, args: values }); - } - - database.transaction(queries, function (error, results) { - if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message)); - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results[results.length - 1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found')); - - return callback(null); - }); -} - -function setHealth(appId, health, healthTime, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof health, 'string'); - assert(util.types.isDate(healthTime)); - assert.strictEqual(typeof callback, 'function'); - - const values = { health, healthTime }; - - updateWithConstraints(appId, values, '', callback); -} - -function setTask(appId, values, options, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof values, 'object'); - assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); - - values.ts = new Date(); - - if (!options.requireNullTaskId) return updateWithConstraints(appId, values, '', callback); - - if (options.requiredState === null) { - updateWithConstraints(appId, values, 'AND taskId IS NULL', callback); - } else { - updateWithConstraints(appId, values, `AND taskId IS NULL AND installationState = "${options.requiredState}"`, callback); - } -} - diff --git a/src/apphealthmonitor.js b/src/apphealthmonitor.js index 140dc7d8f..8a471211a 100644 --- a/src/apphealthmonitor.js +++ b/src/apphealthmonitor.js @@ -1,7 +1,6 @@ 'use strict'; -const appdb = require('./appdb.js'), - apps = require('./apps.js'), +const apps = require('./apps.js'), assert = require('assert'), async = require('async'), auditSource = require('./auditsource.js'), @@ -11,7 +10,8 @@ const appdb = require('./appdb.js'), docker = require('./docker.js'), eventlog = require('./eventlog.js'), safe = require('safetydance'), - superagent = require('superagent'); + superagent = require('superagent'), + util = require('util'); exports = module.exports = { run @@ -56,7 +56,7 @@ function setHealth(app, health, callback) { return callback(null); } - appdb.setHealth(app.id, health, healthTime, function (error) { + util.callbackify(apps.setHealth)(app.id, health, healthTime, function (error) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null); // app uninstalled? if (error) return callback(error); @@ -113,7 +113,7 @@ function getContainerInfo(containerId, callback) { if (!appId) return callback(null, null /* app */, { name: result.Name.slice(1) }); // addon . Name has a '/' in the beginning for some reason - apps.get(appId, callback); // don't get by container id as this can be an exec container + util.callbackify(apps.get)(appId, callback); // don't get by container id as this can be an exec container }); } @@ -169,7 +169,9 @@ function processDockerEvents(intervalSecs, callback) { function processApp(callback) { assert.strictEqual(typeof callback, 'function'); - apps.getAll(function (error, allApps) { + const appsList = util.callbackify(apps.list); + + appsList(function (error, allApps) { if (error) return callback(error); async.each(allApps, checkAppHealth, function (error) { diff --git a/src/apps.js b/src/apps.js index 07714f0f8..6c9710f05 100644 --- a/src/apps.js +++ b/src/apps.js @@ -5,11 +5,19 @@ exports = module.exports = { removeInternalFields, removeRestrictedFields, + // database crud + add, + update, + setHealth, + del, + get, getByIpAddress, getByFqdn, - getAll, - getAllByUser, + list, + listByUser, + + // user actions install, uninstall, @@ -36,7 +44,7 @@ exports = module.exports = { exportApp, clone, - update, + updateApp, backup, listBackups, @@ -106,17 +114,21 @@ exports = module.exports = { HEALTH_ERROR: 'error', HEALTH_DEAD: 'dead', + // subdomain table types + SUBDOMAIN_TYPE_PRIMARY: 'primary', + SUBDOMAIN_TYPE_REDIRECT: 'redirect', + SUBDOMAIN_TYPE_ALIAS: 'alias', + // exported for testing _validatePortBindings: validatePortBindings, _validateAccessRestriction: validateAccessRestriction, _translatePortBindings: translatePortBindings, + _clear: clear }; -const appdb = require('./appdb.js'), - appstore = require('./appstore.js'), +const appstore = require('./appstore.js'), appTaskManager = require('./apptaskmanager.js'), assert = require('assert'), - async = require('async'), backups = require('./backups.js'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), @@ -150,7 +162,15 @@ const appdb = require('./appdb.js'), _ = require('underscore'); const NOOP_CALLBACK = function (error) { if (error) debug(error); }; -const domainsList = util.callbackify(domains.list); + +const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState', + 'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares', + 'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', + 'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp', + 'apps.creationTime', 'apps.updateTime', 'apps.enableMailbox', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate', + 'apps.dataDir', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(','); + +// const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(','); // validate the port bindings function validatePortBindings(portBindings, manifest) { @@ -429,20 +449,17 @@ function removeRestrictedFields(app) { 'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup'); } -function getIcon(app, options, callback) { +async function getIcon(app, options) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); - appdb.getIcons(app.id, function (error, icons) { - if (error) return callback(error); + const icons = await getIcons(app.id); + if (!icons) throw new BoxError(BoxError.NOT_FOUND, 'No such app'); - if (!options.original && icons.icon) return callback(null, icons.icon); + if (!options.original && icons.icon) return icons.icon; + if (icons.appStoreIcon) return icons.appStoreIcon; - if (icons.appStoreIcon) return callback(null, icons.appStoreIcon); - - callback(new BoxError(BoxError.NOT_FOUND, 'No icon')); - }); + return null; } function getMemoryLimit(app) { @@ -459,7 +476,101 @@ function getMemoryLimit(app) { return memoryLimit; } -function postProcess(app, domainObjectMap) { +function postProcess(result) { + assert.strictEqual(typeof result, 'object'); + + assert(result.manifestJson === null || typeof result.manifestJson === 'string'); + result.manifest = safe.JSON.parse(result.manifestJson); + delete result.manifestJson; + + assert(result.tagsJson === null || typeof result.tagsJson === 'string'); + result.tags = safe.JSON.parse(result.tagsJson) || []; + delete result.tagsJson; + + assert(result.reverseProxyConfigJson === null || typeof result.reverseProxyConfigJson === 'string'); + result.reverseProxyConfig = safe.JSON.parse(result.reverseProxyConfigJson) || {}; + delete result.reverseProxyConfigJson; + + assert(result.hostPorts === null || typeof result.hostPorts === 'string'); + assert(result.environmentVariables === null || typeof result.environmentVariables === 'string'); + + result.portBindings = { }; + let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(','); + let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(','); + let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(','); + + delete result.hostPorts; + delete result.environmentVariables; + delete result.portTypes; + + for (let i = 0; i < environmentVariables.length; i++) { + result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] }; + } + + assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string'); + result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson); + if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = []; + delete result.accessRestrictionJson; + + result.sso = !!result.sso; + result.enableBackup = !!result.enableBackup; + result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; + result.enableMailbox = !!result.enableMailbox; + result.proxyAuth = !!result.proxyAuth; + result.hasIcon = !!result.hasIcon; + result.hasAppStoreIcon = !!result.hasAppStoreIcon; + + assert(result.debugModeJson === null || typeof result.debugModeJson === 'string'); + result.debugMode = safe.JSON.parse(result.debugModeJson); + delete result.debugModeJson; + + assert(result.servicesConfigJson === null || typeof result.servicesConfigJson === 'string'); + result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {}; + delete result.servicesConfigJson; + + let subdomains = JSON.parse(result.subdomains), domains = JSON.parse(result.domains), subdomainTypes = JSON.parse(result.subdomainTypes); + delete result.subdomains; + delete result.domains; + delete result.subdomainTypes; + + result.alternateDomains = []; + result.aliasDomains = []; + for (let i = 0; i < subdomainTypes.length; i++) { + if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_PRIMARY) { + result.location = subdomains[i]; + result.domain = domains[i]; + } else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_REDIRECT) { + result.alternateDomains.push({ domain: domains[i], subdomain: subdomains[i] }); + } else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_ALIAS) { + result.aliasDomains.push({ domain: domains[i], subdomain: subdomains[i] }); + } + } + + let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues); + delete result.envNames; + delete result.envValues; + result.env = {}; + for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty + if (envNames[i]) result.env[envNames[i]] = envValues[i]; + } + + let volumeIds = JSON.parse(result.volumeIds); + delete result.volumeIds; + let volumeReadOnlys = JSON.parse(result.volumeReadOnlys); + delete result.volumeReadOnlys; + + result.mounts = volumeIds[0] === null ? [] : volumeIds.map((v, idx) => { return { volumeId: v, readOnly: !!volumeReadOnlys[idx] }; }); // NOTE: volumeIds is [null] when volumes of an app is empty + + result.error = safe.JSON.parse(result.errorJson); + delete result.errorJson; + + result.taskId = result.taskId ? String(result.taskId) : null; +} + +function attachProperties(app, domainObjectMap) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof domainObjectMap, 'object'); + let result = {}; for (let portName in app.portBindings) { result[portName] = app.portBindings[portName].hostPort; @@ -471,136 +582,341 @@ function postProcess(app, domainObjectMap) { app.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); } -function hasAccessTo(app, user, callback) { +function hasAccessTo(app, user) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof user, 'object'); - assert.strictEqual(typeof callback, 'function'); - if (app.accessRestriction === null) return callback(null, true); + if (app.accessRestriction === null) return true; // check user access - if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return callback(null, true); + if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return true; - if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) return callback(null, true); // admins can always access any app + if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) return true; // admins can always access any app - if (!app.accessRestriction.groups) return callback(null, false); + if (!app.accessRestriction.groups) return false; - if (app.accessRestriction.groups.some(function (gid) { return Array.isArray(user.groupIds) && user.groupIds.indexOf(gid) !== -1; })) return callback(null, true); + if (app.accessRestriction.groups.some(function (gid) { return Array.isArray(user.groupIds) && user.groupIds.indexOf(gid) !== -1; })) return true; - callback(null, false); + return false; } -function getDomainObjectMap(callback) { - assert.strictEqual(typeof callback, 'function'); +async function add(id, appStoreId, manifest, location, domain, portBindings, data) { + assert.strictEqual(typeof id, 'string'); + assert.strictEqual(typeof appStoreId, 'string'); + assert(manifest && typeof manifest === 'object'); + assert.strictEqual(typeof manifest.version, 'string'); + assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof portBindings, 'object'); + assert(data && typeof data === 'object'); - domainsList(function (error, domainObjects) { - if (error) return callback(error); + portBindings = portBindings || { }; - let domainObjectMap = {}; - for (let d of domainObjects) { domainObjectMap[d.domain] = d; } + const manifestJson = JSON.stringify(manifest); + const accessRestriction = data.accessRestriction || null; + const accessRestrictionJson = JSON.stringify(accessRestriction); + const memoryLimit = data.memoryLimit || 0; + const cpuShares = data.cpuShares || 512; + const installationState = data.installationState; + const runState = data.runState; + const sso = 'sso' in data ? data.sso : null; + const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null; + const env = data.env || {}; + const label = data.label || null; + const tagsJson = data.tags ? JSON.stringify(data.tags) : null; + const mailboxName = data.mailboxName || null; + const mailboxDomain = data.mailboxDomain || null; + const reverseProxyConfigJson = data.reverseProxyConfig ? JSON.stringify(data.reverseProxyConfig) : null; + const servicesConfigJson = data.servicesConfig ? JSON.stringify(data.servicesConfig) : null; + const enableMailbox = data.enableMailbox || false; + const icon = data.icon || null; - callback(null, domainObjectMap); + let queries = []; + + queries.push({ + query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, ' + + 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, enableMailbox) ' + + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, + sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, enableMailbox ] }); -} -function get(appId, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof callback, 'function'); + queries.push({ + query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', + args: [ id, domain, location, exports.SUBDOMAIN_TYPE_PRIMARY ] + }); - getDomainObjectMap(function (error, domainObjectMap) { - if (error) return callback(error); - - appdb.get(appId, function (error, app) { - if (error) return callback(error); - - postProcess(app, domainObjectMap); - - callback(null, app); + Object.keys(portBindings).forEach(function (env) { + queries.push({ + query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)', + args: [ env, portBindings[env].hostPort, portBindings[env].type, id ] }); }); + + Object.keys(env).forEach(function (name) { + queries.push({ + query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)', + args: [ id, name, env[name] ] + }); + }); + + if (data.alternateDomains) { + data.alternateDomains.forEach(function (d) { + queries.push({ + query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', + args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ] + }); + }); + } + + if (data.aliasDomains) { + data.aliasDomains.forEach(function (d) { + queries.push({ + query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', + args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_ALIAS ] + }); + }); + } + + const [error] = await safe(database.transaction(queries)); + if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, error.message); + if (error && error.code === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, 'no such domain'); + if (error) throw new BoxError(BoxError.DATABASE_ERROR, error); +} + +async function getIcons(id) { + assert.strictEqual(typeof id, 'string'); + + const results = await database.query('SELECT icon, appStoreIcon FROM apps WHERE id = ?', [ id ]); + if (results.length === 0) return null; + return { icon: results[0].icon, appStoreIcon: results[0].appStoreIcon }; +} + +async function updateWithConstraints(id, app, constraints) { + assert.strictEqual(typeof id, 'string'); + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof constraints, 'string'); + assert(!('portBindings' in app) || typeof app.portBindings === 'object'); + assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === ''); + assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains)); + assert(!('aliasDomains' in app) || Array.isArray(app.aliasDomains)); + assert(!('tags' in app) || Array.isArray(app.tags)); + assert(!('env' in app) || typeof app.env === 'object'); + + const queries = [ ]; + + if ('portBindings' in app) { + var portBindings = app.portBindings || { }; + // replace entries by app id + queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] }); + Object.keys(portBindings).forEach(function (env) { + var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ]; + queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values }); + }); + } + + if ('env' in app) { + queries.push({ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] }); + + Object.keys(app.env).forEach(function (name) { + queries.push({ + query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)', + args: [ id, name, app.env[name] ] + }); + }); + } + + if ('location' in app && 'domain' in app) { // must be updated together as they are unique together + queries.push({ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ]}); // all locations of an app must be updated together + queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.location, exports.SUBDOMAIN_TYPE_PRIMARY ]}); + + if ('alternateDomains' in app) { + app.alternateDomains.forEach(function (d) { + queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]}); + }); + } + + if ('aliasDomains' in app) { + app.aliasDomains.forEach(function (d) { + queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_ALIAS ]}); + }); + } + } + + if ('mounts' in app) { + queries.push({ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ]}); + app.mounts.forEach(function (m) { + queries.push({ query: 'INSERT INTO appMounts (appId, volumeId, readOnly) VALUES (?, ?, ?)', args: [ id, m.volumeId, m.readOnly ]}); + }); + } + + const fields = [ ], values = [ ]; + for (let p in app) { + if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig') { + fields.push(`${p}Json = ?`); + values.push(JSON.stringify(app[p])); + } else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'aliasDomains' && p !== 'env' && p !== 'mounts') { + fields.push(p + ' = ?'); + values.push(app[p]); + } + } + + if (values.length !== 0) { + values.push(id); + queries.push({ query: 'UPDATE apps SET ' + fields.join(', ') + ' WHERE id = ? ' + constraints, args: values }); + } + + const [error, results] = await safe(database.transaction(queries)); + if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, error.message); + if (error) throw new BoxError(BoxError.DATABASE_ERROR, error); + if (results[results.length - 1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'App not found'); +} + +async function update(id, app) { + // ts is useful as a versioning mechanism (for example, icon changed). update the timestamp explicity in code instead of db. + // this way health and healthTime can be updated without changing ts + app.ts = new Date(); + await updateWithConstraints(id, app, ''); +} + +async function setHealth(appId, health, healthTime) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof health, 'string'); + assert(util.types.isDate(healthTime)); + + await updateWithConstraints(appId, { health, healthTime }, ''); +} + +async function setTask(appId, values, options) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof values, 'object'); + assert.strictEqual(typeof options, 'object'); + + values.ts = new Date(); + + if (!options.requireNullTaskId) return await updateWithConstraints(appId, values, ''); + + if (options.requiredState === null) { + await updateWithConstraints(appId, values, 'AND taskId IS NULL'); + } else { + await updateWithConstraints(appId, values, `AND taskId IS NULL AND installationState = "${options.requiredState}"`); + } +} + +async function del(id) { + assert.strictEqual(typeof id, 'string'); + + const queries = [ + { query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] }, + { query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] }, + { query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] }, + { query: 'DELETE FROM appPasswords WHERE identifier = ?', args: [ id ] }, + { query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ] }, + { query: 'DELETE FROM apps WHERE id = ?', args: [ id ] } + ]; + + const results = await database.transaction(queries); + if (results[5].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'App not found'); +} + +async function clear() { + await database.query('DELETE FROM subdomains'); + await database.query('DELETE FROM appPortBindings'); + await database.query('DELETE FROM appAddonConfigs'); + await database.query('DELETE FROM appEnvVars'); + await database.query('DELETE FROM apps'); +} + +async function getDomainObjectMap() { + const domainObjects = await domains.list(); + let domainObjectMap = {}; + for (let d of domainObjects) { domainObjectMap[d.domain] = d; } + return domainObjectMap; +} + +// each query simply join apps table with another table by id. we then join the full result together +const PB_QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id'; +const ENV_QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id'; +const SUBDOMAIN_QUERY = 'SELECT id, JSON_ARRAYAGG(subdomains.subdomain) AS subdomains, JSON_ARRAYAGG(subdomains.domain) AS domains, JSON_ARRAYAGG(subdomains.type) AS subdomainTypes FROM apps LEFT JOIN subdomains ON apps.id = subdomains.appId GROUP BY apps.id'; +const MOUNTS_QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id'; +const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, subdomains, domains, subdomainTypes, volumeIds, volumeReadOnlys FROM apps` + + ` LEFT JOIN (${PB_QUERY}) AS q1 on q1.id = apps.id` + + ` LEFT JOIN (${ENV_QUERY}) AS q2 on q2.id = apps.id` + + ` LEFT JOIN (${SUBDOMAIN_QUERY}) AS q3 on q3.id = apps.id` + + ` LEFT JOIN (${MOUNTS_QUERY}) AS q4 on q4.id = apps.id`; + +async function get(id) { + assert.strictEqual(typeof id, 'string'); + + const domainObjectMap = await getDomainObjectMap(); + + const result = await database.query(`${APPS_QUERY} WHERE apps.id = ?`, [ id ]); + if (result.length === 0) return null; + + postProcess(result[0]); + attachProperties(result[0], domainObjectMap); + + return result[0]; } // returns the app associated with this IP (app or scheduler) -function getByIpAddress(ip, callback) { +async function getByIpAddress(ip) { assert.strictEqual(typeof ip, 'string'); - assert.strictEqual(typeof callback, 'function'); - appdb.getByIpAddress(ip, function (error, app) { - if (error) return callback(error); + const domainObjectMap = await getDomainObjectMap(); - getDomainObjectMap(function (error, domainObjectMap) { - if (error) return callback(error); + const result = await database.query(`${APPS_QUERY} WHERE apps.containerIp = ?`, [ ip ]); + if (result.length === 0) return null; - postProcess(app, domainObjectMap); - - callback(null, app); - }); - }); + postProcess(result[0]); + attachProperties(result[0], domainObjectMap); + return result[0]; } -function getByFqdn(fqdn, callback) { +async function list() { + const domainObjectMap = await getDomainObjectMap(); + + const results = await database.query(`${APPS_QUERY} ORDER BY apps.id`, [ ]); + results.forEach(postProcess); + results.forEach((app) => attachProperties(app, domainObjectMap)); + return results; +} + +async function getByFqdn(fqdn) { assert.strictEqual(typeof fqdn, 'string'); - assert.strictEqual(typeof callback, 'function'); - getAll(function (error, result) { - if (error) return callback(error); - - var app = result.find(function (a) { return a.fqdn === fqdn; }); - if (!app) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app')); - - callback(null, app); - }); + const result = await list(); + const app = result.find(function (a) { return a.fqdn === fqdn; }); + return app; } -function getAll(callback) { - assert.strictEqual(typeof callback, 'function'); - - getDomainObjectMap(function (error, domainObjectMap) { - if (error) return callback(error); - - appdb.getAll(function (error, apps) { - if (error) return callback(error); - - apps.forEach((app) => postProcess(app, domainObjectMap)); - - callback(null, apps); - }); - }); -} - -function getAllByUser(user, callback) { +async function listByUser(user) { assert.strictEqual(typeof user, 'object'); - assert.strictEqual(typeof callback, 'function'); - getAll(function (error, result) { - if (error) return callback(error); - - async.filter(result, function (app, iteratorDone) { - hasAccessTo(app, user, iteratorDone); - }, callback); - }); + const result = await list(); + return result.filter((app) => hasAccessTo(app, user)); } -function downloadManifest(appStoreId, manifest, callback) { - if (!appStoreId && !manifest) return callback(new BoxError(BoxError.BAD_FIELD, 'Neither manifest nor appStoreId provided')); +async function downloadManifest(appStoreId, manifest) { + if (!appStoreId && !manifest) throw new BoxError(BoxError.BAD_FIELD, 'Neither manifest nor appStoreId provided'); - if (!appStoreId) return callback(null, '', manifest); + if (!appStoreId) return { appStoreId: '', manifest }; - var parts = appStoreId.split('@'); + const parts = appStoreId.split('@'); - var url = settings.apiServerOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : ''); + const url = settings.apiServerOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : ''); debug('downloading manifest from %s', url); - superagent.get(url).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message)); + const [error, response] = await safe(superagent.get(url).timeout(30 * 1000).ok(() => true)); - if (result.status !== 200) return callback(new BoxError(BoxError.NOT_FOUND, util.format('Failed to get app info from store.', result.status, result.text))); + if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message); - if (!result.body.manifest || typeof result.body.manifest !== 'object') return callback(new BoxError(BoxError.NOT_FOUND, util.format('Missing manifest. Failed to get app info from store.', result.status, result.text))); + if (response.status !== 200) throw new BoxError(BoxError.NOT_FOUND, `Failed to get app info from store. status: ${response.status} text: ${response.text}`); - callback(null, parts[0], result.body.manifest); - }); + if (!response.body.manifest || typeof response.body.manifest !== 'object') throw new BoxError(BoxError.NOT_FOUND, `Missing manifest. Failed to get app info from store. status: ${response.status} text: ${response.text}`); + + return { appStoreId: parts[0], manifest: response.body.manifest }; } function mailboxNameForLocation(location, manifest) { @@ -630,7 +946,7 @@ function scheduleTask(appId, installationState, taskId, callback) { const options = { timeout: 20 * 60 * 60 * 1000 /* 20 hours */, nice: 15, memoryLimit }; - appTaskManager.scheduleTask(appId, taskId, options, function (error) { + appTaskManager.scheduleTask(appId, taskId, options, async function (error) { debug(`scheduleTask: task ${taskId} of ${appId} completed`); if (error && (error.code === tasks.ECRASHED || error.code === tasks.ESTOPPED)) { // if task crashed, update the error debug(`Apptask crashed/stopped: ${error.message}`); @@ -640,21 +956,19 @@ function scheduleTask(appId, installationState, taskId, callback) { // see also apptask makeTaskError boxError.details.taskId = taskId; boxError.details.installationState = installationState; - appdb.update(appId, { installationState: exports.ISTATE_ERROR, error: boxError.toPlainObject(), taskId: null }, callback.bind(null, error)); + await safe(update(appId, { installationState: exports.ISTATE_ERROR, error: boxError.toPlainObject(), taskId: null })); } else if (!(installationState === exports.ISTATE_PENDING_UNINSTALL && !error)) { // clear out taskId except for successful uninstall - appdb.update(appId, { taskId: null }, callback.bind(null, error)); - } else { - callback(error); + await safe(update(appId, { taskId: null })); } + callback(error); }); }); } -function addTask(appId, installationState, task, callback) { +async function addTask(appId, installationState, task) { assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof installationState, 'string'); assert.strictEqual(typeof task, 'object'); // { args, values } - assert.strictEqual(typeof callback, 'function'); const { args, values } = task; // TODO: match the SQL logic to match checkAppState. this means checking the error.installationState and installationState. Unfortunately, former is JSON right now @@ -662,20 +976,15 @@ function addTask(appId, installationState, task, callback) { const scheduleNow = 'scheduleNow' in task ? task.scheduleNow : true; const requireNullTaskId = 'requireNullTaskId' in task ? task.requireNullTaskId : true; - const tasksAdd = util.callbackify(tasks.add); + const taskId = await tasks.add(tasks.TASK_APP, [ appId, args ]); - tasksAdd(tasks.TASK_APP, [ appId, args ], function (error, taskId) { - if (error) return callback(error); + const [updateError] = await safe(setTask(appId, _.extend({ installationState, taskId, error: null }, values), { requiredState, requireNullTaskId })); + if (updateError && updateError.reason === BoxError.NOT_FOUND) throw new BoxError(BoxError.BAD_STATE, 'Another task is scheduled for this app'); // could be because app went away OR a taskId exists + if (updateError) throw updateError; - appdb.setTask(appId, _.extend({ installationState, taskId, error: null }, values), { requiredState, requireNullTaskId }, function (error) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.BAD_STATE, 'Another task is scheduled for this app')); // could be because app went away OR a taskId exists - if (error) return callback(error); + if (scheduleNow) scheduleTask(appId, installationState, taskId, task.onFinished || NOOP_CALLBACK); - if (scheduleNow) scheduleTask(appId, installationState, taskId, task.onFinished || NOOP_CALLBACK); - - callback(null, { taskId }); - }); - }); + return taskId; } function checkAppState(app, state) { @@ -700,38 +1009,34 @@ function checkAppState(app, state) { return null; } -function validateLocations(locations, callback) { +async function validateLocations(locations) { assert(Array.isArray(locations)); - assert.strictEqual(typeof callback, 'function'); - getDomainObjectMap(function (error, domainObjectMap) { - if (error) return callback(error); + const domainObjectMap = await getDomainObjectMap(); - for (let location of locations) { - if (!(location.domain in domainObjectMap)) return callback(new BoxError(BoxError.BAD_FIELD, 'No such domain', { field: 'location', domain: location.domain, subdomain: location.subdomain })); + for (let location of locations) { + if (!(location.domain in domainObjectMap)) throw new BoxError(BoxError.BAD_FIELD, 'No such domain', { field: 'location', domain: location.domain, subdomain: location.subdomain }); - let subdomain = location.subdomain; - if (location.type === 'alias' && subdomain.startsWith('*')) { - if (subdomain === '*') continue; - subdomain = subdomain.replace(/^\*\./, ''); // remove *. - } - - error = dns.validateHostname(subdomain, domainObjectMap[location.domain]); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Bad location: ' + error.message, { field: 'location', domain: location.domain, subdomain: location.subdomain })); + let subdomain = location.subdomain; + if (location.type === 'alias' && subdomain.startsWith('*')) { + if (subdomain === '*') continue; + subdomain = subdomain.replace(/^\*\./, ''); // remove *. } - callback(null, domainObjectMap); - }); + const error = dns.validateHostname(subdomain, domainObjectMap[location.domain]); + if (error) throw new BoxError(BoxError.BAD_FIELD, 'Bad location: ' + error.message, { field: 'location', domain: location.domain, subdomain: location.subdomain }); + } + + return domainObjectMap; } function hasMailAddon(manifest) { return manifest.addons.sendmail || manifest.addons.recvmail; } -function install(data, auditSource, callback) { +async function install(data, auditSource) { assert(data && typeof data === 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof data.manifest, 'object'); // manifest is already downloaded @@ -757,44 +1062,44 @@ function install(data, auditSource, callback) { manifest = data.manifest; let error = manifestFormat.parse(manifest); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Manifest error: ' + error.message)); + if (error) throw new BoxError(BoxError.BAD_FIELD, `Manifest error: ${error.message}`); error = checkManifestConstraints(manifest); - if (error) return callback(error); + if (error) throw error; error = validatePortBindings(portBindings, manifest); - if (error) return callback(error); + if (error) throw error; error = validateAccessRestriction(accessRestriction); - if (error) return callback(error); + if (error) throw error; error = validateMemoryLimit(manifest, memoryLimit); - if (error) return callback(error); + if (error) throw error; error = validateDebugMode(debugMode); - if (error) return callback(error); + if (error) throw error; error = validateLabel(label); - if (error) return callback(error); + if (error) throw error; error = validateTags(tags); - if (error) return callback(error); + if (error) throw error; - if ('sso' in data && !('optionalSso' in manifest)) return callback(new BoxError(BoxError.BAD_FIELD, 'sso can only be specified for apps with optionalSso')); + if ('sso' in data && !('optionalSso' in manifest)) throw new BoxError(BoxError.BAD_FIELD, 'sso can only be specified for apps with optionalSso'); // if sso was unspecified, enable it by default if possible if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['proxyAuth']; error = validateEnv(env); - if (error) return callback(error); + if (error) throw error; - if (settings.isDemo() && constants.DEMO_BLACKLISTED_APPS.includes(appStoreId)) return callback(new BoxError(BoxError.BAD_FIELD, 'This app is blacklisted in the demo')); + if (settings.isDemo() && constants.DEMO_BLACKLISTED_APPS.includes(appStoreId)) throw new BoxError(BoxError.BAD_FIELD, 'This app is blacklisted in the demo'); const mailboxName = hasMailAddon(manifest) ? mailboxNameForLocation(location, manifest) : null; const mailboxDomain = hasMailAddon(manifest) ? domain : null; const appId = uuid.v4(); if (icon) { - if (!validator.isBase64(icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' })); + if (!validator.isBase64(icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' }); icon = Buffer.from(icon, 'base64'); } @@ -802,272 +1107,227 @@ function install(data, auditSource, callback) { .concat(alternateDomains.map(ad => _.extend(ad, { type: 'redirect' }))) .concat(aliasDomains.map(ad => _.extend(ad, { type: 'alias' }))); - validateLocations(locations, function (error, domainObjectMap) { - if (error) return callback(error); + const domainObjectMap = await validateLocations(locations); - debug('Will install app with id : ' + appId); + debug('Will install app with id : ' + appId); - const data = { - accessRestriction, - memoryLimit, - sso, - debugMode, - mailboxName, - mailboxDomain, - enableBackup, - enableAutomaticUpdate, - alternateDomains, - aliasDomains, - env, - label, - tags, - icon, - enableMailbox, - runState: exports.RSTATE_RUNNING, - installationState: exports.ISTATE_PENDING_INSTALL - }; + const app = { + accessRestriction, + memoryLimit, + sso, + debugMode, + mailboxName, + mailboxDomain, + enableBackup, + enableAutomaticUpdate, + alternateDomains, + aliasDomains, + env, + label, + tags, + icon, + enableMailbox, + runState: exports.RSTATE_RUNNING, + installationState: exports.ISTATE_PENDING_INSTALL + }; - appdb.add(appId, 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); + const [addError] = await safe(add(appId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), app)); + if (addError && addError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(addError.message, locations, domainObjectMap, portBindings); + if (addError) throw addError; - purchaseApp({ appId: appId, appstoreId: appStoreId, manifestId: manifest.id || 'customapp' }, function (error) { - if (error) return callback(error); + await purchaseApp({ appId, appstoreId: appStoreId, manifestId: manifest.id || 'customapp' }); - const task = { - args: { restoreConfig: null, skipDnsSetup, overwriteDns }, - values: { }, - requiredState: data.installationState - }; + const task = { + args: { restoreConfig: null, skipDnsSetup, overwriteDns }, + values: { }, + requiredState: app.installationState + }; - addTask(appId, data.installationState, task, function (error, result) { - if (error) return callback(error); + const taskId = await addTask(appId, app.installationState, task); - const newApp = _.extend({}, _.omit(data, 'icon'), { appStoreId, manifest, location, domain, portBindings }); - newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]); - newApp.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - newApp.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + const newApp = _.extend({}, _.omit(app, 'icon'), { appStoreId, manifest, location, domain, portBindings }); + newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]); + newApp.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + newApp.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId, app: newApp, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId, app: newApp, taskId }); - callback(null, { id : appId, taskId: result.taskId }); - }); - }); - }); - }); + return { id : appId, taskId }; } -function setAccessRestriction(app, accessRestriction, auditSource, callback) { +async function setAccessRestriction(app, accessRestriction, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof accessRestriction, 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; let error = validateAccessRestriction(accessRestriction); - if (error) return callback(error); + if (error) throw error; - appdb.update(appId, { accessRestriction }, function (error) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, accessRestriction }); - - callback(); - }); + await update(appId, { accessRestriction }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, accessRestriction }); } -function setLabel(app, label, auditSource, callback) { +async function setLabel(app, label, auditSource) { 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); + if (error) throw error; - appdb.update(appId, { label }, function (error) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, label }); - - callback(); - }); + await update(appId, { label }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, label }); } -function setTags(app, tags, auditSource, callback) { +async function setTags(app, tags, auditSource) { 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); + if (error) throw error; - appdb.update(appId, { tags }, function (error) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, tags }); - - callback(); - }); + await update(appId, { tags }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, tags }); } -function setIcon(app, icon, auditSource, callback) { +async function setIcon(app, icon, auditSource) { 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 (!validator.isBase64(icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' }); icon = Buffer.from(icon, 'base64'); } - appdb.update(appId, { icon }, function (error) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, iconChanged: true }); - callback(); - }); + await update(appId, { icon }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, iconChanged: true }); } -function setMemoryLimit(app, memoryLimit, auditSource, callback) { +async function setMemoryLimit(app, memoryLimit, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof memoryLimit, 'number'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; let error = checkAppState(app, exports.ISTATE_PENDING_RESIZE); - if (error) return callback(error); + if (error) throw error; error = validateMemoryLimit(app.manifest, memoryLimit); - if (error) return callback(error); + if (error) throw error; const task = { args: {}, values: { memoryLimit } }; - addTask(appId, exports.ISTATE_PENDING_RESIZE, task, function (error, result) { - if (error) return callback(error); + const taskId = await addTask(appId, exports.ISTATE_PENDING_RESIZE, task); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, memoryLimit, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, memoryLimit, taskId }); - callback(null, { taskId: result.taskId }); - }); + return { taskId }; } -function setCpuShares(app, cpuShares, auditSource, callback) { +async function setCpuShares(app, cpuShares, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof cpuShares, 'number'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; let error = checkAppState(app, exports.ISTATE_PENDING_RESIZE); - if (error) return callback(error); + if (error) throw error; error = validateCpuShares(cpuShares); - if (error) return callback(error); + if (error) throw error; const task = { args: {}, values: { cpuShares } }; - addTask(appId, exports.ISTATE_PENDING_RESIZE, task, function (error, result) { - if (error) return callback(error); + const taskId = await safe(addTask(appId, exports.ISTATE_PENDING_RESIZE, task)); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cpuShares, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cpuShares, taskId }); - callback(null, { taskId: result.taskId }); - }); + return { taskId }; } -function setMounts(app, mounts, auditSource, callback) { +async function setMounts(app, mounts, auditSource) { assert.strictEqual(typeof app, 'object'); assert(Array.isArray(mounts)); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER); - if (error) return callback(error); + if (error) throw error; const task = { args: {}, values: { mounts } }; - addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) { - if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(new BoxError(BoxError.CONFLICT, 'Duplicate mount points')); - if (error) return callback(error); + const [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task)); + if (taskError && taskError.reason === BoxError.ALREADY_EXISTS) throw new BoxError(BoxError.CONFLICT, 'Duplicate mount points'); + if (taskError) throw taskError; - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mounts, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mounts, taskId }); - callback(null, { taskId: result.taskId }); - }); + return { taskId }; } -function setEnvironment(app, env, auditSource, callback) { +async function setEnvironment(app, env, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof env, 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER); - if (error) return callback(error); + if (error) throw error; error = validateEnv(env); - if (error) return callback(error); + if (error) throw error; const task = { args: {}, values: { env } }; - addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) { - if (error) return callback(error); + const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, env, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, env, taskId }); - callback(null, { taskId: result.taskId }); - }); + return { taskId }; } -function setDebugMode(app, debugMode, auditSource, callback) { +async function setDebugMode(app, debugMode, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof debugMode, 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; let error = checkAppState(app, exports.ISTATE_PENDING_DEBUG); - if (error) return callback(error); + if (error) throw error; error = validateDebugMode(debugMode); - if (error) return callback(error); + if (error) throw error; const task = { args: {}, values: { debugMode } }; - addTask(appId, exports.ISTATE_PENDING_DEBUG, task, function (error, result) { - if (error) return callback(error); + const taskId = await addTask(appId, exports.ISTATE_PENDING_DEBUG, task); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, debugMode, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, debugMode, taskId }); - callback(null, { taskId: result.taskId }); - }); + return { taskId }; } -function setMailbox(app, data, auditSource, callback) { +async function setMailbox(app, data, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const { enable, mailboxDomain } = data; let mailboxName = data.mailboxName; @@ -1077,66 +1337,48 @@ function setMailbox(app, data, auditSource, callback) { const appId = app.id; let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER); - if (error) return callback(error); + if (error) throw error; - if (!hasMailAddon(app.manifest)) return callback(new BoxError(BoxError.BAD_FIELD, 'App does not use mail addons')); + if (!hasMailAddon(app.manifest)) throw new BoxError(BoxError.BAD_FIELD, 'App does not use mail addons'); - const getDomainFunc = util.callbackify(mail.getDomain); + await mail.getDomain(mailboxDomain); // check if domain exists - getDomainFunc(mailboxDomain, function (error) { - if (error) return callback(error); + if (mailboxName) { + error = mail.validateName(mailboxName); + if (error) throw new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' }); + } else { + mailboxName = mailboxNameForLocation(app.location, app.domain, app.manifest); + } - 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.domain, app.manifest); - } + const task = { + args: {}, + values: { enableMailbox: enable, mailboxName, mailboxDomain } + }; + const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task); - const task = { - args: {}, - values: { enableMailbox: enable, mailboxName, mailboxDomain } - }; - addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) { - if (error) return callback(error); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mailboxName, taskId }); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mailboxName, taskId: result.taskId }); - - callback(null, { taskId: result.taskId }); - }); - }); + return { taskId }; } -function setAutomaticBackup(app, enable, auditSource, callback) { +async function setAutomaticBackup(app, enable, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof enable, 'boolean'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; - appdb.update(appId, { enableBackup: enable }, function (error) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableBackup: enable }); - - callback(); - }); + await update(appId, { enableBackup: enable }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableBackup: enable }); } -function setAutomaticUpdate(app, enable, auditSource, callback) { +async function setAutomaticUpdate(app, enable, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof enable, 'boolean'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; - appdb.update(appId, { enableAutomaticUpdate: enable }, function (error) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableAutomaticUpdate: enable }); - - callback(); - }); + await update(appId, { enableAutomaticUpdate: enable }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableAutomaticUpdate: enable }); } async function setReverseProxyConfig(app, reverseProxyConfig, auditSource) { @@ -1155,16 +1397,15 @@ async function setReverseProxyConfig(app, reverseProxyConfig, auditSource) { await reverseProxy.writeAppConfig(_.extend({}, app, { reverseProxyConfig })); - await util.promisify(appdb.update)(appId, { reverseProxyConfig }); + await update(appId, { reverseProxyConfig }); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, reverseProxyConfig }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, reverseProxyConfig }); } async function setCertificate(app, data, auditSource) { assert.strictEqual(typeof app, 'object'); assert(data && typeof data === 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; const { location, domain, cert, key } = data; @@ -1177,18 +1418,17 @@ async function setCertificate(app, data, auditSource) { } await reverseProxy.setAppCertificateSync(location, domainObject, { cert, key }); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cert, key }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cert, key }); } -function setLocation(app, data, auditSource, callback) { +async function setLocation(app, data, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; let error = checkAppState(app, exports.ISTATE_PENDING_LOCATION_CHANGE); - if (error) return callback(error); + if (error) throw error; let values = { location: data.location.toLowerCase(), @@ -1201,7 +1441,7 @@ function setLocation(app, data, auditSource, callback) { if ('portBindings' in data) { error = validatePortBindings(data.portBindings, app.manifest); - if (error) return callback(error); + if (error) throw error; values.portBindings = translatePortBindings(data.portBindings || null, app.manifest); } @@ -1224,44 +1464,40 @@ function setLocation(app, data, auditSource, callback) { .concat(values.alternateDomains.map(ad => _.extend(ad, { type: 'redirect' }))) .concat(values.aliasDomains.map(ad => _.extend(ad, { type: 'alias' }))); - validateLocations(locations, function (error, domainObjectMap) { - if (error) return callback(error); + const domainObjectMap = await validateLocations(locations); - const task = { - args: { - oldConfig: _.pick(app, 'location', 'domain', 'fqdn', 'alternateDomains', 'aliasDomains', 'portBindings'), - skipDnsSetup: !!data.skipDnsSetup, - 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); + const task = { + args: { + oldConfig: _.pick(app, 'location', 'domain', 'fqdn', 'alternateDomains', 'aliasDomains', 'portBindings'), + skipDnsSetup: !!data.skipDnsSetup, + overwriteDns: !!data.overwriteDns + }, + values + }; + let [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_LOCATION_CHANGE, task)); + if (taskError && taskError.reason === BoxError.ALREADY_EXISTS) taskError = getDuplicateErrorDetails(error.message, locations, domainObjectMap, data.portBindings); + if (taskError) throw taskError; - values.fqdn = dns.fqdn(values.location, domainObjectMap[values.domain]); - values.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - values.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + values.fqdn = dns.fqdn(values.location, domainObjectMap[values.domain]); + values.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + values.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, _.extend({ appId, app, taskId: result.taskId }, values)); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, _.extend({ appId, app, taskId }, values)); - callback(null, { taskId: result.taskId }); - }); - }); + return { taskId }; } -function setDataDir(app, dataDir, auditSource, callback) { +async function setDataDir(app, dataDir, auditSource) { 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); + if (error) throw error; error = validateDataDir(dataDir); - if (error) return callback(error); + if (error) throw error; const task = { args: { newDataDir: dataDir }, @@ -1270,21 +1506,18 @@ function setDataDir(app, dataDir, auditSource, callback) { if (!error) services.rebuildService('sftp', NOOP_CALLBACK); } }; - addTask(appId, exports.ISTATE_PENDING_DATA_DIR_MIGRATION, task, function (error, result) { - if (error) return callback(error); + const taskId = await addTask(appId, exports.ISTATE_PENDING_DATA_DIR_MIGRATION, task); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, dataDir, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, dataDir, taskId }); - callback(null, { taskId: result.taskId }); - }); + return { taskId }; } -function update(app, data, auditSource, callback) { +async function updateApp(app, data, auditSource) { assert.strictEqual(typeof app, 'object'); assert(data && typeof data === 'object'); assert(data.manifest && typeof data.manifest === 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const skipBackup = !!data.skipBackup, appId = app.id, @@ -1293,42 +1526,42 @@ function update(app, data, auditSource, callback) { let values = {}; - if (app.runState === exports.RSTATE_STOPPED) return callback(new BoxError(BoxError.BAD_STATE, 'Stopped apps cannot be updated')); + if (app.runState === exports.RSTATE_STOPPED) throw new BoxError(BoxError.BAD_STATE, 'Stopped apps cannot be updated'); let error = checkAppState(app, exports.ISTATE_PENDING_UPDATE); - if (error) return callback(error); + if (error) throw error; error = manifestFormat.parse(manifest); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Manifest error:' + error.message)); + if (error) throw new BoxError(BoxError.BAD_FIELD, 'Manifest error:' + error.message); error = checkManifestConstraints(manifest); - if (error) return callback(error); + if (error) throw error; - var updateConfig = { skipBackup, manifest, appStoreId }; // this will clear appStoreId when updating from a repo and set it if passed in for update route + const updateConfig = { skipBackup, manifest, appStoreId }; // this will clear appStoreId when updating from a repo and set it if passed in for update route // 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')); + if (!data.force) throw new BoxError(BoxError.BAD_FIELD, 'manifest id does not match. 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 (!data.force) throw 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 (!validator.isBase64(data.icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' }); data.icon = Buffer.from(data.icon, 'base64'); } values.icon = data.icon; } // 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')); + if (app.debugMode && !data.force) throw 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 @@ -1350,13 +1583,11 @@ function update(app, data, auditSource, callback) { eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditSource, { app, toManifest: manifest, fromManifest: app.manifest, success: !error, errorMessage: error ? error.message : null }); } }; - addTask(appId, exports.ISTATE_PENDING_UPDATE, task, function (error, result) { - if (error) return callback(error); + const taskId = await addTask(appId, exports.ISTATE_PENDING_UPDATE, task); - eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId, app, skipBackup, toManifest: manifest, fromManifest: app.manifest, force: data.force, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId, app, skipBackup, toManifest: manifest, fromManifest: app.manifest, force: data.force, taskId }); - callback(null, { taskId: result.taskId }); - }); + return { taskId }; } function getLocalLogfilePaths(app) { @@ -1430,11 +1661,10 @@ async function getCertificate(subdomain, domain) { // 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(app, data, auditSource, callback) { +async function repair(app, data, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof data, 'object'); // { manifest } assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; let errorState = (app.error && app.error.installationState) || exports.ISTATE_PENDING_CONFIGURE; @@ -1450,10 +1680,10 @@ function repair(app, data, auditSource, callback) { task.args = { skipDnsSetup: false, overwriteDns: true }; if (data.manifest) { let error = manifestFormat.parse(data.manifest); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, `manifest error: ${error.message}`)); + if (error) throw new BoxError(BoxError.BAD_FIELD, `manifest error: ${error.message}`); error = checkManifestConstraints(data.manifest); - if (error) return callback(error); + if (error) throw error; if (!hasMailAddon(data.manifest)) { // clear if repair removed addon task.values.mailboxName = task.values.mailboxDomain = null; @@ -1473,13 +1703,11 @@ function repair(app, data, auditSource, callback) { } } - addTask(appId, errorState, task, function (error, result) { - if (error) return callback(error); + const taskId = await addTask(appId, errorState, task); - eventlog.add(eventlog.ACTION_APP_REPAIR, auditSource, { app, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_REPAIR, auditSource, { app, taskId }); - callback(null, { taskId: result.taskId }); - }); + return { taskId }; } async function restore(app, backupId, auditSource) { @@ -1522,22 +1750,17 @@ async function restore(app, backupId, auditSource) { values }; - return new Promise((resolve, reject) => { - addTask(appId, exports.ISTATE_PENDING_RESTORE, task, function (error, result) { - if (error) return reject(error); + const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTORE, task); - eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest, taskId }); - resolve({ taskId: result.taskId }); - }); - }); + return { taskId }; } -function importApp(app, data, auditSource, callback) { +async function importApp(app, data, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; @@ -1548,91 +1771,76 @@ function importApp(app, data, auditSource, callback) { const { backupId, backupFormat, backupConfig } = data; let error = backupFormat ? validateBackupFormat(backupFormat) : null; - if (error) return callback(error); + if (error) throw error; error = checkAppState(app, exports.ISTATE_PENDING_IMPORT); - if (error) return callback(error); + if (error) throw error; + + const testProviderConfig = util.promisify(backups.testProviderConfig); // 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(); + if (backupConfig) await testProviderConfig(backupConfig); - testBackupConfig(function (error) { - if (error) return callback(error); - - if (backupConfig) { - if ('password' in backupConfig) { - backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password); - delete backupConfig.password; - } else { - backupConfig.encryption = null; - } + if (backupConfig) { + if ('password' in backupConfig) { + backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password); + delete backupConfig.password; + } else { + backupConfig.encryption = null; } + } - const restoreConfig = { backupId, backupFormat, backupConfig }; + const restoreConfig = { backupId, backupFormat, backupConfig }; - const task = { - args: { - restoreConfig, - oldManifest: app.manifest, - skipDnsSetup: false, - overwriteDns: true - }, - values: {} - }; - addTask(appId, exports.ISTATE_PENDING_IMPORT, task, function (error, result) { - if (error) return callback(error); + const task = { + args: { + restoreConfig, + oldManifest: app.manifest, + skipDnsSetup: false, + overwriteDns: true + }, + values: {} + }; + const taskId = await addTask(appId, exports.ISTATE_PENDING_IMPORT, task); - eventlog.add(eventlog.ACTION_APP_IMPORT, auditSource, { app: app, backupId, fromManifest: app.manifest, toManifest: app.manifest, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_IMPORT, auditSource, { app: app, backupId, fromManifest: app.manifest, toManifest: app.manifest, taskId }); - callback(null, { taskId: result.taskId }); - }); - }); + return { taskId }; } -function exportApp(app, data, auditSource, callback) { +async function exportApp(app, data, auditSource) { assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof callback, 'function'); + assert.strictEqual(typeof data, 'object'); + assert.strictEqual(typeof auditSource, 'object'); const appId = app.id; let error = checkAppState(app, exports.ISTATE_PENDING_BACKUP); - if (error) return callback(error); + if (error) throw error; const task = { args: { snapshotOnly: true }, values: {} }; - addTask(appId, exports.ISTATE_PENDING_BACKUP, task, (error, result) => { - if (error) return callback(error); - callback(null, { taskId: result.taskId }); - }); + const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task); + return { taskId }; } -function purchaseApp(data, callback) { +async function purchaseApp(data) { assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof callback, 'function'); - const purchaseApp = util.callbackify(appstore.purchaseApp); + const [purchaseError] = await safe(appstore.purchaseApp(data)); + if (!purchaseError) return; - purchaseApp(data, function (error) { - if (!error) return callback(); - - // if purchase failed, rollback the appdb record - appdb.del(data.appId, function (delError) { - if (delError) debug('install: Failed to rollback app installation.', delError); - - callback(error); - }); - }); + await del(data.appId); } -function clone(app, data, user, auditSource, callback) { +async function clone(app, data, user, auditSource) { 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'); const location = data.location.toLowerCase(), domain = data.domain.toLowerCase(), @@ -1648,179 +1856,149 @@ function clone(app, data, user, auditSource, callback) { assert.strictEqual(typeof portBindings, 'object'); const locations = [{ subdomain: location, domain, type: 'primary' }]; - validateLocations(locations, async function (error, domainObjectMap) { - if (error) return callback(error); + const domainObjectMap = await validateLocations(locations); + const backupInfo = await backups.get(backupId); - const [backupsError, backupInfo] = await safe(backups.get(backupId)); - if (backupsError) return callback(backupsError); + if (!backupInfo.manifest) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config'); + if (backupInfo.encryptionVersion === 1) throw new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and cannot be cloned'); - if (!backupInfo.manifest) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config')); - if (backupInfo.encryptionVersion === 1) return callback(new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and cannot be cloned')); + const manifest = backupInfo.manifest, appStoreId = app.appStoreId; - const manifest = backupInfo.manifest, appStoreId = app.appStoreId; + // re-validate because this new box version may not accept old configs + let error = checkManifestConstraints(manifest); + if (error) throw error; - // 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) throw error; - error = validatePortBindings(portBindings, manifest); - if (error) return callback(error); + // should we copy the original app's mailbox settings instead? + let mailboxName = hasMailAddon(manifest) ? mailboxNameForLocation(location, manifest) : null; + let mailboxDomain = hasMailAddon(manifest) ? domain : null; - // should we copy the original app's mailbox settings instead? - let mailboxName = hasMailAddon(manifest) ? mailboxNameForLocation(location, manifest) : null; - let mailboxDomain = hasMailAddon(manifest) ? domain : null; + const newAppId = uuid.v4(); - const newAppId = uuid.v4(); + const icons = await getIcons(app.id); - appdb.getIcons(app.id, function (error, icons) { - if (error) return callback(error); + const obj = { + installationState: exports.ISTATE_PENDING_CLONE, + runState: exports.RSTATE_RUNNING, + memoryLimit: app.memoryLimit, + cpuShares: app.cpuShares, + accessRestriction: app.accessRestriction, + sso: !!app.sso, + mailboxName: mailboxName, + mailboxDomain: mailboxDomain, + enableBackup: app.enableBackup, + reverseProxyConfig: app.reverseProxyConfig, + env: app.env, + alternateDomains: [], + aliasDomains: [], + servicesConfig: app.servicesConfig, + label: app.label ? `${app.label}-clone` : '', + tags: app.tags, + enableAutomaticUpdate: app.enableAutomaticUpdate, + icon: icons.icon, + enableMailbox: app.enableMailbox + }; - const data = { - installationState: exports.ISTATE_PENDING_CLONE, - runState: exports.RSTATE_RUNNING, - memoryLimit: app.memoryLimit, - cpuShares: app.cpuShares, - accessRestriction: app.accessRestriction, - sso: !!app.sso, - mailboxName: mailboxName, - mailboxDomain: mailboxDomain, - enableBackup: app.enableBackup, - reverseProxyConfig: app.reverseProxyConfig, - env: app.env, - alternateDomains: [], - aliasDomains: [], - servicesConfig: app.servicesConfig, - label: app.label ? `${app.label}-clone` : '', - tags: app.tags, - enableAutomaticUpdate: app.enableAutomaticUpdate, - icon: icons.icon, - enableMailbox: app.enableMailbox - }; + const [addError] = await safe(add(newAppId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), obj)); + if (addError && addError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(addError.message, locations, domainObjectMap, portBindings); + if (addError) throw addError; - 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); + await purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id || 'customapp' }); - purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id || 'customapp' }, function (error) { - if (error) return callback(error); + const restoreConfig = { backupId: backupId, backupFormat: backupInfo.format }; + const task = { + args: { restoreConfig, overwriteDns, skipDnsSetup, oldManifest: null }, + values: {}, + requiredState: exports.ISTATE_PENDING_CLONE + }; + const taskId = await addTask(newAppId, exports.ISTATE_PENDING_CLONE, task); - const restoreConfig = { backupId: backupId, backupFormat: backupInfo.format }; - const task = { - args: { restoreConfig, overwriteDns, skipDnsSetup, 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({}, _.omit(obj, 'icon'), { appStoreId, manifest, location, domain, portBindings }); + newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]); + newApp.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + newApp.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - const newApp = _.extend({}, _.omit(data, 'icon'), { appStoreId, manifest, location, domain, portBindings }); - newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]); - newApp.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - newApp.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); + await eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: newApp, taskId }); - 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 }); - }); - }); - }); - }); - }); + return { id: newAppId, taskId }; } -function uninstall(app, auditSource, callback) { +async function uninstall(app, auditSource) { 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_UNINSTALL); - if (error) return callback(error); + if (error) throw error; - const unpurchaseApp = util.callbackify(appstore.unpurchaseApp); + await appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id || 'customapp' }); - 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 + }; + const taskId = await addTask(appId, exports.ISTATE_PENDING_UNINSTALL, task); + await eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId, app, taskId }); - 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 }); - }); - }); + return { taskId }; } -function start(app, auditSource, callback) { +async function start(app, auditSource) { 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_START); - if (error) return callback(error); + if (error) throw error; const task = { args: {}, values: { runState: exports.RSTATE_RUNNING } }; - addTask(appId, exports.ISTATE_PENDING_START, task, function (error, result) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_APP_START, auditSource, { appId, app, taskId: result.taskId }); - - callback(null, { taskId: result.taskId }); - }); + const taskId = await addTask(appId, exports.ISTATE_PENDING_START, task); + await eventlog.add(eventlog.ACTION_APP_START, auditSource, { appId, app, taskId }); + return { taskId }; } -function stop(app, auditSource, callback) { +async function stop(app, auditSource) { 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_STOP); - if (error) return callback(error); + if (error) throw error; const task = { args: {}, values: { runState: exports.RSTATE_STOPPED } }; - addTask(appId, exports.ISTATE_PENDING_STOP, task, function (error, result) { - if (error) return callback(error); + const taskId = await addTask(appId, exports.ISTATE_PENDING_STOP, task); - eventlog.add(eventlog.ACTION_APP_STOP, auditSource, { appId, app, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_STOP, auditSource, { appId, app, taskId }); - callback(null, { taskId: result.taskId }); - }); + return { taskId }; } -function restart(app, auditSource, callback) { +async function restart(app, auditSource) { 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); + if (error) throw 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 taskId = await addTask(appId, exports.ISTATE_PENDING_RESTART, task); - eventlog.add(eventlog.ACTION_APP_RESTART, auditSource, { appId, app, taskId: result.taskId }); + await eventlog.add(eventlog.ACTION_APP_RESTART, auditSource, { appId, app, taskId }); - callback(null, { taskId: result.taskId }); - }); + return { taskId }; } function checkManifestConstraints(manifest) { @@ -1917,55 +2095,47 @@ function canAutoupdateApp(app, updateInfo) { return true; } -function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is { appId -> { manifest } } +async function autoupdateApps(updateInfo, auditSource) { // updateInfo is { appId -> { manifest } } assert.strictEqual(typeof updateInfo, 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - async.eachSeries(Object.keys(updateInfo), function iterator(appId, iteratorDone) { - get(appId, function (error, app) { - if (error) { - debug('Cannot autoupdate app %s : %s', appId, error.message); - return iteratorDone(); - } + for (const appId of Object.keys(updateInfo)) { + const [getError, app] = await safe(get(appId)); + if (getError) { + debug(`Cannot autoupdate app ${appId}: ${getError.message}`); + continue; + } - if (!canAutoupdateApp(app, updateInfo[appId])) { - debug(`app ${app.fqdn} requires manual update`); - return iteratorDone(); - } + if (!canAutoupdateApp(app, updateInfo[appId])) { + debug(`app ${app.fqdn} requires manual update`); + continue; + } - var data = { - manifest: updateInfo[appId].manifest, - force: false - }; + const data = { + manifest: updateInfo[appId].manifest, + force: false + }; - update(app, data, auditSource, function (error) { - if (error) debug('Error initiating autoupdate of %s. %s', appId, error.message); - - iteratorDone(null); - }); - }); - }, callback); + const [updateError] = await safe(updateApp(app, data, auditSource)); + if (updateError) debug(`Error initiating autoupdate of ${appId}. ${updateError.message}`); + } } -function backup(app, callback) { +async function backup(app) { assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof callback, 'function'); const appId = app.id; let error = checkAppState(app, exports.ISTATE_PENDING_BACKUP); - if (error) return callback(error); + if (error) throw error; const task = { args: {}, values: {} }; - addTask(appId, exports.ISTATE_PENDING_BACKUP, task, (error, result) => { - if (error) return callback(error); + const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task); - callback(null, { taskId: result.taskId }); - }); + return { taskId }; } async function listBackups(app, page, perPage) { @@ -1976,131 +2146,103 @@ async function listBackups(app, page, perPage) { return await backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, page, perPage); } -function restoreInstalledApps(options, callback) { +async function restoreInstalledApps(options) { assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); - const addTaskAsync = util.promisify(addTask); + let apps = await list(); + apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand + apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTORE); // safeguard against tasks being created non-stop if we crash on startup - getAll(async function (error, apps) { - if (error) return callback(error); - - apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand - apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTORE); // safeguard against tasks being created non-stop if we crash on startup - - for (const app of apps) { - const [error, results] = await safe(backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, 1, 1)); - let installationState, restoreConfig, oldManifest; - if (!error && results.length) { - installationState = exports.ISTATE_PENDING_RESTORE; - restoreConfig = { backupId: results[0].id, backupFormat: results[0].format }; - oldManifest = app.manifest; - } else { - installationState = exports.ISTATE_PENDING_INSTALL; - restoreConfig = null; - oldManifest = null; - } - - const task = { - args: { restoreConfig, skipDnsSetup: options.skipDnsSetup, overwriteDns: true, oldManifest }, - values: {}, - scheduleNow: false, // task will be scheduled by autoRestartTasks when platform is ready - requireNullTaskId: false // ignore existing stale taskId - }; - - debug(`restoreInstalledApps: marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`); - - const [addTaskError, result] = await safe(addTaskAsync(app.id, installationState, task)); - if (addTaskError) debug(`restoreInstalledApps: error marking ${app.fqdn} for restore: ${JSON.stringify(addTaskError)}`); - else debug(`restoreInstalledApps: marked ${app.id} for restore with taskId ${result.taskId}`); + for (const app of apps) { + const [error, results] = await safe(backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, 1, 1)); + let installationState, restoreConfig, oldManifest; + if (!error && results.length) { + installationState = exports.ISTATE_PENDING_RESTORE; + restoreConfig = { backupId: results[0].id, backupFormat: results[0].format }; + oldManifest = app.manifest; + } else { + installationState = exports.ISTATE_PENDING_INSTALL; + restoreConfig = null; + oldManifest = null; } - callback(null); - }); + const task = { + args: { restoreConfig, skipDnsSetup: options.skipDnsSetup, overwriteDns: true, oldManifest }, + values: {}, + scheduleNow: false, // task will be scheduled by autoRestartTasks when platform is ready + requireNullTaskId: false // ignore existing stale taskId + }; + + debug(`restoreInstalledApps: marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`); + + const [addTaskError, taskId] = await safe(addTask(app.id, installationState, task)); + if (addTaskError) debug(`restoreInstalledApps: error marking ${app.fqdn} for restore: ${JSON.stringify(addTaskError)}`); + else debug(`restoreInstalledApps: marked ${app.id} for restore with taskId ${taskId}`); + } } -function configureInstalledApps(callback) { - assert.strictEqual(typeof callback, 'function'); +async function configureInstalledApps() { + let apps = await list(); + apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand + apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_CONFIGURE); // safeguard against tasks being created non-stop if we crash on startup - getAll(function (error, apps) { - if (error) return callback(error); + for (const app of apps) { + debug(`configureInstalledApps: marking ${app.fqdn} for reconfigure`); - apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand - apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_CONFIGURE); // safeguard against tasks being created non-stop if we crash on startup + const task = { + args: {}, + values: {}, + scheduleNow: false, // task will be scheduled by autoRestartTasks when platform is ready + requireNullTaskId: false // ignore existing stale taskId + }; - async.eachSeries(apps, function (app, iteratorDone) { - debug(`configureInstalledApps: marking ${app.fqdn} for reconfigure`); - - const task = { - args: {}, - values: {}, - scheduleNow: false, // task will be scheduled by autoRestartTasks when platform is ready - requireNullTaskId: false // ignore existing stale taskId - }; - - addTask(app.id, exports.ISTATE_PENDING_CONFIGURE, task, function (error, result) { - if (error) debug(`configureInstalledApps: error marking ${app.fqdn} for configure: ${JSON.stringify(error)}`); - else debug(`configureInstalledApps: marked ${app.id} for re-configure with taskId ${result.taskId}`); - - iteratorDone(); // ignore error - }); - }, callback); - }); + const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_CONFIGURE, task)); + if (addTaskError) debug(`configureInstalledApps: error marking ${app.fqdn} for configure: ${JSON.stringify(addTaskError)}`); + else debug(`configureInstalledApps: marked ${app.id} for re-configure with taskId ${taskId}`); + } } -function restartAppsUsingAddons(changedAddons, callback) { +async function restartAppsUsingAddons(changedAddons) { assert(Array.isArray(changedAddons)); - assert.strictEqual(typeof callback, 'function'); - getAll(function (error, apps) { - if (error) return callback(error); + const stopContainers = util.promisify(docker.stopContainers); - apps = apps.filter(app => app.manifest.addons && _.intersection(Object.keys(app.manifest.addons), changedAddons).length !== 0); - apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand - apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTART); // safeguard against tasks being created non-stop restart if we crash on startup - apps = apps.filter(app => app.runState !== exports.RSTATE_STOPPED); // don't start stopped apps + let apps = await list(); + apps = apps.filter(app => app.manifest.addons && _.intersection(Object.keys(app.manifest.addons), changedAddons).length !== 0); + apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand + apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTART); // safeguard against tasks being created non-stop restart if we crash on startup + apps = apps.filter(app => app.runState !== exports.RSTATE_STOPPED); // don't start stopped apps - async.eachSeries(apps, function (app, iteratorDone) { - debug(`restartAppsUsingAddons: marking ${app.fqdn} for restart`); + for (const app of apps) { + debug(`restartAppsUsingAddons: marking ${app.fqdn} for restart`); - const task = { - args: {}, - values: { runState: exports.RSTATE_RUNNING } - }; + const task = { + args: {}, + values: { runState: exports.RSTATE_RUNNING } + }; - // stop apps before updating the databases because postgres will "lock" them preventing import - docker.stopContainers(app.id, function (error) { - if (error) debug(`restartAppsUsingAddons: error stopping ${app.fqdn}`, error); + // stop apps before updating the databases because postgres will "lock" them preventing import + const [stopError] = await safe(stopContainers(app.id)); + if (stopError) debug(`restartAppsUsingAddons: error stopping ${app.fqdn}`, stopError); - addTask(app.id, exports.ISTATE_PENDING_RESTART, task, function (error, result) { - if (error) debug(`restartAppsUsingAddons: error marking ${app.fqdn} for restart: ${JSON.stringify(error)}`); - else debug(`restartAppsUsingAddons: marked ${app.id} for restart with taskId ${result.taskId}`); - - iteratorDone(); // ignore error - }); - }); - }, callback); - }); + const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_RESTART, task)); + if (addTaskError) debug(`restartAppsUsingAddons: error marking ${app.fqdn} for restart: ${JSON.stringify(addTaskError)}`); + else debug(`restartAppsUsingAddons: marked ${app.id} for restart with taskId ${taskId}`); + } } // auto-restart app tasks after a crash -function schedulePendingTasks(callback) { - assert.strictEqual(typeof callback, 'function'); - +async function schedulePendingTasks() { debug('schedulePendingTasks: scheduling app tasks'); - getAll(function (error, result) { - if (error) return callback(error); + const result = list(); - result.forEach(function (app) { - if (!app.taskId) return; // if not in any pending state, do nothing + result.forEach(function (app) { + if (!app.taskId) return; // if not in any pending state, do nothing - debug(`schedulePendingTasks: schedule task for ${app.fqdn} ${app.id}: state=${app.installationState},taskId=${app.taskId}`); + debug(`schedulePendingTasks: schedule task for ${app.fqdn} ${app.id}: state=${app.installationState},taskId=${app.taskId}`); - scheduleTask(app.id, app.installationState, app.taskId, NOOP_CALLBACK); - }); - - callback(null); + scheduleTask(app.id, app.installationState, app.taskId, NOOP_CALLBACK); }); } @@ -2198,24 +2340,19 @@ function uploadFile(app, sourceFilePath, destFilePath, callback) { }); } -function backupConfig(app, callback) { +async function backupConfig(app) { assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof callback, 'function'); if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(app))) { - return callback(new BoxError(BoxError.FS_ERROR, 'Error creating config.json: ' + safe.error.message)); + throw new BoxError(BoxError.FS_ERROR, 'Error creating config.json: ' + safe.error.message); } - appdb.getIcons(app.id, function (error, icons) { - if (!error && icons.icon) safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/icon.png'), icons.icon); - - callback(null); - }); + const [error, icons] = await safe(getIcons(app.id)); + if (!error && icons.icon) safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/icon.png'), icons.icon); } -function restoreConfig(app, callback) { +async function restoreConfig(app) { assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof callback, 'function'); const appConfig = safe.JSON.parse(safe.fs.readFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'))); let data = {}; @@ -2226,5 +2363,5 @@ function restoreConfig(app, callback) { const icon = safe.fs.readFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/icon.png')); if (icon) data.icon = icon; - appdb.update(app.id, data, callback); + await update(app.id, data); } diff --git a/src/appstore.js b/src/appstore.js index 956ae6cd2..ec944cf3e 100644 --- a/src/appstore.js +++ b/src/appstore.js @@ -374,7 +374,7 @@ async function createTicket(info, auditSource) { await safe(support.enableRemoteSupport(true, auditSource)); } - info.app = info.appId ? await util.promisify(apps.get)(info.appId) : null; + info.app = info.appId ? await apps.get(info.appId) : null; info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets const request = superagent.post(`${settings.apiServerOrigin()}/api/v1/ticket`) diff --git a/src/apptask.js b/src/apptask.js index 26d16b73b..d0501987e 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -12,8 +12,7 @@ exports = module.exports = { _waitForDnsPropagation: waitForDnsPropagation }; -const appdb = require('./appdb.js'), - apps = require('./apps.js'), +const apps = require('./apps.js'), assert = require('assert'), async = require('async'), auditSource = require('./auditsource.js'), @@ -70,7 +69,7 @@ function updateApp(app, values, callback) { assert.strictEqual(typeof values, 'object'); assert.strictEqual(typeof callback, 'function'); - appdb.update(app.id, values, function (error) { + util.callbackify(apps.update)(app.id, values, function (error) { if (error) return callback(error); for (var value in values) { @@ -403,7 +402,7 @@ function install(app, args, progressCallback, callback) { docker.deleteImage(oldManifest, done); }, - // allocating container ip here, lets the users "repair" an app if allocation fails at appdb.add time + // allocating container ip here, lets the users "repair" an app if allocation fails at apps.add time allocateContainerIp.bind(null, app), progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }), @@ -447,7 +446,7 @@ function install(app, args, progressCallback, callback) { backuptask.downloadApp.bind(null, app, restoreConfig, (progress) => { progressCallback({ percent: 65, message: progress.message }); }), - (done) => { if (app.installationState === apps.ISTATE_PENDING_IMPORT) apps.restoreConfig(app, done); else done(); }, + async () => { if (app.installationState === apps.ISTATE_PENDING_IMPORT) await apps.restoreConfig(app); }, progressCallback.bind(null, { percent: 70, message: 'Restoring addons' }), services.restoreAddons.bind(null, app, app.manifest.addons) ], next); @@ -759,7 +758,7 @@ function update(app, args, progressCallback, callback) { async.each(Object.keys(currentPorts), function (portName, callback) { if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use - appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) { + apps.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) { if (error && error.reason === BoxError.NOT_FOUND) debugApp(app, 'update: portbinding does not exist in database', error); else if (error) return next(error); @@ -914,7 +913,7 @@ function uninstall(app, args, progressCallback, callback) { cleanupLogs.bind(null, app), progressCallback.bind(null, { percent: 95, message: 'Remove app from database' }), - appdb.del.bind(null, app.id) + apps.del.bind(null, app.id) ], function seriesDone(error) { if (error) { debugApp(app, 'error uninstalling app:', error); @@ -931,7 +930,7 @@ function run(appId, args, progressCallback, callback) { assert.strictEqual(typeof callback, 'function'); // determine what to do - apps.get(appId, function (error, app) { + util.callbackify(apps.get)(appId, function (error, app) { if (error) return callback(error); debugApp(app, `startTask installationState: ${app.installationState} runState: ${app.runState}`); diff --git a/src/backupcleaner.js b/src/backupcleaner.js index 9d446cdf0..e22ba8997 100644 --- a/src/backupcleaner.js +++ b/src/backupcleaner.js @@ -8,9 +8,7 @@ exports = module.exports = { const apps = require('./apps.js'), assert = require('assert'), - async = require('async'), backups = require('./backups.js'), - BoxError = require('./boxerror.js'), constants = require('./constants.js'), debug = require('debug')('box:backupcleaner'), moment = require('moment'), @@ -117,46 +115,41 @@ async function cleanupBackup(backupConfig, backup, progressCallback) { }); } -function cleanupAppBackups(backupConfig, referencedAppBackupIds, progressCallback, callback) { +async function cleanupAppBackups(backupConfig, referencedAppBackupIds, progressCallback) { assert.strictEqual(typeof backupConfig, 'object'); assert(Array.isArray(referencedAppBackupIds)); assert.strictEqual(typeof progressCallback, 'function'); - assert.strictEqual(typeof callback, 'function'); let removedAppBackupIds = []; - apps.getAll(async function (error, allApps) { - if (error) return callback(error); + const allApps = await apps.list(); + const allAppIds = allApps.map(a => a.id); - const allAppIds = allApps.map(a => a.id); + const appBackups = await backups.getByTypePaged(backups.BACKUP_TYPE_APP, 1, 1000); - const [getError, appBackups] = await safe(backups.getByTypePaged(backups.BACKUP_TYPE_APP, 1, 1000)); - if (getError) return callback(getError); + // collate the backups by app id. note that the app could already have been uninstalled + let appBackupsById = {}; + for (const appBackup of appBackups) { + if (!appBackupsById[appBackup.identifier]) appBackupsById[appBackup.identifier] = []; + appBackupsById[appBackup.identifier].push(appBackup); + } - // collate the backups by app id. note that the app could already have been uninstalled - let appBackupsById = {}; - for (const appBackup of appBackups) { - if (!appBackupsById[appBackup.identifier]) appBackupsById[appBackup.identifier] = []; - appBackupsById[appBackup.identifier].push(appBackup); - } + // apply backup policy per app. keep latest backup only for existing apps + let appBackupsToRemove = []; + for (const appId of Object.keys(appBackupsById)) { + applyBackupRetentionPolicy(appBackupsById[appId], _.extend({ keepLatest: allAppIds.includes(appId) }, backupConfig.retentionPolicy), referencedAppBackupIds); + appBackupsToRemove = appBackupsToRemove.concat(appBackupsById[appId].filter(b => !b.keepReason)); + } - // apply backup policy per app. keep latest backup only for existing apps - let appBackupsToRemove = []; - for (const appId of Object.keys(appBackupsById)) { - applyBackupRetentionPolicy(appBackupsById[appId], _.extend({ keepLatest: allAppIds.includes(appId) }, backupConfig.retentionPolicy), referencedAppBackupIds); - appBackupsToRemove = appBackupsToRemove.concat(appBackupsById[appId].filter(b => !b.keepReason)); - } + for (const appBackup of appBackupsToRemove) { + await progressCallback({ message: `Removing app backup (${appBackup.identifier}): ${appBackup.id}`}); + removedAppBackupIds.push(appBackup.id); + await cleanupBackup(backupConfig, appBackup, progressCallback); // never errors + } - for (const appBackup of appBackupsToRemove) { - progressCallback({ message: `Removing app backup (${appBackup.identifier}): ${appBackup.id}`}); - removedAppBackupIds.push(appBackup.id); - await cleanupBackup(backupConfig, appBackup, progressCallback); // never errors - } + debug('cleanupAppBackups: done'); - debug('cleanupAppBackups: done'); - - callback(null, removedAppBackupIds); - }); + return removedAppBackupIds; } async function cleanupBoxBackups(backupConfig, progressCallback) { @@ -175,7 +168,7 @@ async function cleanupBoxBackups(backupConfig, progressCallback) { continue; } - progressCallback({ message: `Removing box backup ${boxBackup.id}`}); + await progressCallback({ message: `Removing box backup ${boxBackup.id}`}); removedBoxBackupIds.push(boxBackup.id); await cleanupBackup(backupConfig, boxBackup, progressCallback); @@ -206,7 +199,7 @@ async function cleanupMissingBackups(backupConfig, progressCallback) { const [existsError, exists] = await safe(backupExists(backupConfig, backupFilePath)); if (existsError || exists) continue; - progressCallback({ message: `Removing missing backup ${backup.id}`}); + await progressCallback({ message: `Removing missing backup ${backup.id}`}); const [delError] = await safe(backups.del(backup.id)); if (delError) debug(`cleanupBackup: error removing ${backup.id} from database`, delError); @@ -219,19 +212,20 @@ async function cleanupMissingBackups(backupConfig, progressCallback) { } // removes the snapshots of apps that have been uninstalled -function cleanupSnapshots(backupConfig, callback) { +async function cleanupSnapshots(backupConfig) { assert.strictEqual(typeof backupConfig, 'object'); - assert.strictEqual(typeof callback, 'function'); - var contents = safe.fs.readFileSync(paths.SNAPSHOT_INFO_FILE, 'utf8'); - var info = safe.JSON.parse(contents); - if (!info) return callback(); + const contents = safe.fs.readFileSync(paths.SNAPSHOT_INFO_FILE, 'utf8'); + const info = safe.JSON.parse(contents); + if (!info) return; delete info.box; - async.eachSeries(Object.keys(info), function (appId, iteratorDone) { - apps.get(appId, function (error /*, app */) { - if (!error || error.reason !== BoxError.NOT_FOUND) return iteratorDone(); + for (const appId of Object.keys(info)) { + const [, app] = await apps.get(appId); + if (app) continue; // app is still installed + + await new Promise((resolve) => { function done(/* ignoredError */) { safe.fs.unlinkSync(path.join(paths.BACKUP_INFO_DIR, `${appId}.sync.cache`)); safe.fs.unlinkSync(path.join(paths.BACKUP_INFO_DIR, `${appId}.sync.cache.new`)); @@ -239,7 +233,7 @@ function cleanupSnapshots(backupConfig, callback) { backups.setSnapshotInfo(appId, null, function (/* ignoredError */) { debug('cleanupSnapshots: cleaned up snapshot of app id %s', appId); - iteratorDone(); + resolve(); }); } @@ -251,51 +245,32 @@ function cleanupSnapshots(backupConfig, callback) { events.on('done', done); } }); - }, function () { - debug('cleanupSnapshots: done'); + } - callback(); - }); + debug('cleanupSnapshots: done'); } -function run(progressCallback, callback) { +async function run(progressCallback) { assert.strictEqual(typeof progressCallback, 'function'); - assert.strictEqual(typeof callback, 'function'); - const getBackupConfig = util.callbackify(settings.getBackupConfig); + const backupConfig = await settings.getBackupConfig(); - getBackupConfig(async function (error, backupConfig) { - if (error) return callback(error); + if (backupConfig.retentionPolicy.keepWithinSecs < 0) { + debug('cleanup: keeping all backups'); + return {}; + } - if (backupConfig.retentionPolicy.keepWithinSecs < 0) { - debug('cleanup: keeping all backups'); - return callback(null, {}); - } + await progressCallback({ percent: 10, message: 'Cleaning box backups' }); + const { removedBoxBackupIds, referencedAppBackupIds } = await cleanupBoxBackups(backupConfig, progressCallback);; - progressCallback({ percent: 10, message: 'Cleaning box backups' }); + await progressCallback({ percent: 40, message: 'Cleaning app backups' }); + const removedAppBackupIds = await cleanupAppBackups(backupConfig, referencedAppBackupIds, progressCallback); - const [cleanupBoxError, removedBoxBackups ] = await safe(cleanupBoxBackups(backupConfig, progressCallback)); - if (cleanupBoxError) return callback(cleanupBoxError); + await progressCallback({ percent: 70, message: 'Cleaning missing backups' }); + const missingBackupIds = await cleanupMissingBackups(backupConfig, progressCallback); - const { removedBoxBackupIds, referencedAppBackupIds } = removedBoxBackups; + await progressCallback({ percent: 90, message: 'Cleaning snapshots' }); + await cleanupSnapshots(backupConfig); - progressCallback({ percent: 40, message: 'Cleaning app backups' }); - - cleanupAppBackups(backupConfig, referencedAppBackupIds, progressCallback, async function (error, removedAppBackupIds) { - if (error) return callback(error); - - progressCallback({ percent: 70, message: 'Cleaning missing backups' }); - - const [cleanupMissingBackupsError, missingBackupIds] = await cleanupMissingBackups(backupConfig, progressCallback); - if (cleanupMissingBackupsError) return callback(cleanupMissingBackupsError); - - progressCallback({ percent: 90, message: 'Cleaning snapshots' }); - - cleanupSnapshots(backupConfig, function (error) { - if (error) return callback(error); - - callback(null, { removedBoxBackupIds, removedAppBackupIds, missingBackupIds }); - }); - }); - }); + return { removedBoxBackupIds, removedAppBackupIds, missingBackupIds }; } diff --git a/src/backuptask.js b/src/backuptask.js index 498e17261..f9c97af70 100644 --- a/src/backuptask.js +++ b/src/backuptask.js @@ -912,7 +912,9 @@ function snapshotApp(app, progressCallback, callback) { const startTime = new Date(); progressCallback({ message: `Snapshotting app ${app.fqdn}` }); - apps.backupConfig(app, function (error) { + const appsBackupConfig = util.callbackify(apps.backupConfig); + + appsBackupConfig(app, function (error) { if (error) return callback(error); services.backupAddons(app, app.manifest.addons, function (error) { @@ -996,7 +998,7 @@ function backupBoxAndApps(options, progressCallback, callback) { const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,''); - apps.getAll(function (error, allApps) { + util.callbackify(apps.list)(function (error, allApps) { if (error) return callback(error); let percent = 1; diff --git a/src/cloudron.js b/src/cloudron.js index 4d8c848a5..6369fb19c 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -265,34 +265,27 @@ function getLogs(unit, options, callback) { return callback(null, transformStream); } -function prepareDashboardDomain(domain, auditSource, callback) { +async function prepareDashboardDomain(domain, auditSource) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); debug(`prepareDashboardDomain: ${domain}`); - if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode')); + if (settings.isDemo()) throw new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'); - domainsGet(domain, function (error, domainObject) { - if (error) return callback(error); + const domainObject = domains.get(domain); - const fqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject); + const fqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject); - apps.getAll(async function (error, result) { - if (error) return callback(error); + const result = apps.list(); + const conflict = result.filter(app => app.fqdn === fqdn); + if (conflict.length) throw new BoxError(BoxError.BAD_STATE, 'Dashboard location conflicts with an existing app'); - const conflict = result.filter(app => app.fqdn === fqdn); - if (conflict.length) return callback(new BoxError(BoxError.BAD_STATE, 'Dashboard location conflicts with an existing app')); + const taskId = await tasks.add(tasks.TASK_SETUP_DNS_AND_CERT, [ constants.DASHBOARD_LOCATION, domain, auditSource ]); - const [taskError, taskId] = await safe(tasks.add(tasks.TASK_SETUP_DNS_AND_CERT, [ constants.DASHBOARD_LOCATION, domain, auditSource ])); - if (taskError) return callback(taskError); + tasks.startTask(taskId, {}, NOOP_CALLBACK); - tasks.startTask(taskId, {}, NOOP_CALLBACK); - - callback(null, taskId); - }); - }); + return taskId; } // call this only pre activation since it won't start mail server diff --git a/src/cron.js b/src/cron.js index b79654f4d..c9b22beee 100644 --- a/src/cron.js +++ b/src/cron.js @@ -29,6 +29,7 @@ const appHealthMonitor = require('./apphealthmonitor.js'), dyndns = require('./dyndns.js'), eventlog = require('./eventlog.js'), janitor = require('./janitor.js'), + safe = require('safetydance'), scheduler = require('./scheduler.js'), settings = require('./settings.js'), system = require('./system.js'), @@ -184,19 +185,19 @@ function autoupdatePatternChanged(pattern, tz) { gJobs.autoUpdater = new CronJob({ cronTime: pattern, - onTick: function() { + onTick: async function() { const updateInfo = updateChecker.getUpdateInfo(); // do box before app updates. for the off chance that the box logic fixes some app update logic issue if (updateInfo.box && !updateInfo.box.unstable) { debug('Starting box autoupdate to %j', updateInfo.box); - updater.updateToLatest({ skipBackup: false }, auditSource.CRON, NOOP_CALLBACK); + await safe(updater.updateToLatest({ skipBackup: false }, auditSource.CRON)); return; } const appUpdateInfo = _.omit(updateInfo, 'box'); if (Object.keys(appUpdateInfo).length > 0) { debug('Starting app update to %j', appUpdateInfo); - apps.autoupdateApps(appUpdateInfo, auditSource.CRON, NOOP_CALLBACK); + await safe(apps.autoupdateApps(appUpdateInfo, auditSource.CRON)); } else { debug('No app auto updates available'); } diff --git a/src/database.js b/src/database.js index ce0b32fe7..c08e2cb05 100644 --- a/src/database.js +++ b/src/database.js @@ -20,6 +20,7 @@ const assert = require('assert'), debug = require('debug')('box:database'), mysql = require('mysql'), once = require('once'), + shell = require('./shell.js'), util = require('util'); var gConnectionPool = null; @@ -73,14 +74,12 @@ async function uninitialize() { gConnectionPool = null; } -function clear(callback) { - assert.strictEqual(typeof callback, 'function'); - - var cmd = util.format('mysql --host="%s" --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host="%s" --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done', +async function clear() { + const cmd = util.format('mysql --host="%s" --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host="%s" --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done', gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name, gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name); - child_process.exec(cmd, callback); + await shell.promises.exec('clear_database', cmd); } function query() { @@ -140,7 +139,7 @@ function importFromFile(file, callback) { assert.strictEqual(typeof file, 'string'); assert.strictEqual(typeof callback, 'function'); - var cmd = `/usr/bin/mysql -h "${gDatabase.hostname}" -u ${gDatabase.username} -p${gDatabase.password} ${gDatabase.name} < ${file}`; + const cmd = `/usr/bin/mysql -h "${gDatabase.hostname}" -u ${gDatabase.username} -p${gDatabase.password} ${gDatabase.name} < ${file}`; async.series([ query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'), @@ -156,7 +155,7 @@ function exportToFile(file, callback) { // this option must not be set in production cloudrons which still use the old mysqldump const disableColStats = (constants.TEST && require('fs').readFileSync('/etc/lsb-release', 'utf-8').includes('20.04')) ? '--column-statistics=0' : ''; - var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${disableColStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`; + const cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${disableColStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`; child_process.exec(cmd, callback); } diff --git a/src/dns.js b/src/dns.js index 630aed0f6..84d68d38a 100644 --- a/src/dns.js +++ b/src/dns.js @@ -310,7 +310,7 @@ function syncDnsRecords(options, progressCallback, callback) { const mailSubdomain = settings.mailFqdn().substr(0, settings.mailFqdn().length - settings.mailDomain().length - 1); - apps.getAll(function (error, allApps) { + util.callbackify(apps.list)(function (error, allApps) { if (error) return callback(error); let progress = 1, errors = []; diff --git a/src/dockerproxy.js b/src/dockerproxy.js index 77f400f49..143e99ed1 100644 --- a/src/dockerproxy.js +++ b/src/dockerproxy.js @@ -5,7 +5,7 @@ exports = module.exports = { stop }; -var apps = require('./apps.js'), +const apps = require('./apps.js'), assert = require('assert'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), @@ -20,25 +20,24 @@ var apps = require('./apps.js'), safe = require('safetydance'), _ = require('underscore'); -var gHttpServer = null; +let gHttpServer = null; -function authorizeApp(req, res, next) { +async function authorizeApp(req, res, next) { // make the tests pass for now if (constants.TEST) { req.app = { id: 'testappid' }; return next(); } - apps.getByIpAddress(req.connection.remoteAddress, function (error, app) { - if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized')); - if (error) return next(new HttpError(500, error)); + const [error, app] = await apps.getByIpAddress(req.connection.remoteAddress); + if (error) return next(new HttpError(500, error)); + if (!app) return next(new HttpError(401, 'Unauthorized')); - if (!('docker' in app.manifest.addons)) return next(new HttpError(401, 'Unauthorized')); + if (!('docker' in app.manifest.addons)) return next(new HttpError(401, 'Unauthorized')); - req.app = app; + req.app = app; - next(); - }); + next(); } function attachDockerRequest(req, res, next) { diff --git a/src/domains.js b/src/domains.js index b3899f454..c34b56c83 100644 --- a/src/domains.js +++ b/src/domains.js @@ -131,6 +131,7 @@ async function add(domain, data, auditSource) { if (domain.endsWith('.')) throw new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }); if (zoneName) { + console.log('THE ZONE', zoneName); if (!tld.isValid(zoneName)) throw new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' }); if (zoneName.endsWith('.')) throw new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' }); } else { diff --git a/src/dyndns.js b/src/dyndns.js index a686181ad..44693a964 100644 --- a/src/dyndns.js +++ b/src/dyndns.js @@ -4,7 +4,7 @@ exports = module.exports = { sync }; -let apps = require('./apps.js'), +const apps = require('./apps.js'), assert = require('assert'), async = require('async'), constants = require('./constants.js'), @@ -14,7 +14,8 @@ let apps = require('./apps.js'), paths = require('./paths.js'), safe = require('safetydance'), settings = require('./settings.js'), - sysinfo = require('./sysinfo.js'); + sysinfo = require('./sysinfo.js'), + util = require('util'); // called for dynamic dns setups where we have to update the IP function sync(auditSource, callback) { @@ -37,7 +38,7 @@ function sync(auditSource, callback) { debug('refreshDNS: updated admin location'); - apps.getAll(function (error, result) { + util.callbackify(apps.list)(function (error, result) { if (error) return callback(error); async.each(result, function (app, callback) { diff --git a/src/ldap.js b/src/ldap.js index a687ff791..53da2f780 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -10,7 +10,6 @@ exports = module.exports = { const addonConfigs = require('./addonconfigs.js'), assert = require('assert'), apps = require('./apps.js'), - async = require('async'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), debug = require('debug')('box:ldap'), @@ -20,8 +19,7 @@ const addonConfigs = require('./addonconfigs.js'), mail = require('./mail.js'), safe = require('safetydance'), services = require('./services.js'), - users = require('./users.js'), - util = require('util'); + users = require('./users.js'); var gServer = null; @@ -31,7 +29,7 @@ const GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron'; const GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron'; // Will attach req.app if successful -function authenticateApp(req, res, next) { +async function authenticateApp(req, res, next) { const sourceIp = req.connection.ldap.id.split(':')[0]; if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier')); @@ -41,39 +39,29 @@ function authenticateApp(req, res, next) { return next(); } - apps.getByIpAddress(sourceIp, function (error, app) { - if (error) return next(new ldap.OperationsError(error.message)); - if (!app) return next(new ldap.OperationsError('Could not detect app source')); + const [error, app] = await safe(apps.getByIpAddress(sourceIp)); + if (error) return next(new ldap.OperationsError(error.message)); + if (!app) return next(new ldap.OperationsError('Could not detect app source')); - req.app = app; + req.app = app; - next(); - }); + next(); } -function getUsersWithAccessToApp(req, callback) { +async function getUsersWithAccessToApp(req) { assert.strictEqual(typeof req.app, 'object'); - assert.strictEqual(typeof callback, 'function'); - const getAllUsers = util.callbackify(users.list); - - getAllUsers(function (error, result) { - if (error) return callback(new ldap.OperationsError(error.toString())); - - async.filter(result, apps.hasAccessTo.bind(null, req.app), function (error, allowedUsers) { - if (error) return callback(new ldap.OperationsError(error.toString())); - - callback(null, allowedUsers); - }); - }); + const result = await users.list(); + const allowedUsers = result.filter((user) => apps.hasAccessTo(req.app, user)); + return allowedUsers; } // helper function to deal with pagination function finalSend(results, req, res, next) { - var min = 0; - var max = results.length; - var cookie = null; - var pageSize = 0; + let min = 0; + let max = results.length; + let cookie = null; + let pageSize = 0; // check if this is a paging request, if so get the cookie for session info req.controls.forEach(function (control) { @@ -86,7 +74,7 @@ function finalSend(results, req, res, next) { function sendPagedResults(start, end) { start = (start < min) ? min : start; end = (end > max || end < min) ? max : end; - var i; + let i; for (i = start; i < end; i++) { res.send(results[i]); @@ -128,136 +116,132 @@ function finalSend(results, req, res, next) { next(); } -function userSearch(req, res, next) { +async function userSearch(req, res, next) { debug('user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); - getUsersWithAccessToApp(req, function (error, result) { - if (error) return next(error); + const [error, result] = await safe(getUsersWithAccessToApp(req)); + if (error) return next(new ldap.OperationsError(error.toString())); - var results = []; + let results = []; - // send user objects - result.forEach(function (user) { - // skip entries with empty username. Some apps like owncloud can't deal with this - if (!user.username) return; + // send user objects + result.forEach(function (user) { + // skip entries with empty username. Some apps like owncloud can't deal with this + if (!user.username) return; - var dn = ldap.parseDN('cn=' + user.id + ',ou=users,dc=cloudron'); + const dn = ldap.parseDN('cn=' + user.id + ',ou=users,dc=cloudron'); - var memberof = [ GROUP_USERS_DN ]; - if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) memberof.push(GROUP_ADMINS_DN); + const memberof = [ GROUP_USERS_DN ]; + if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) memberof.push(GROUP_ADMINS_DN); - var displayName = user.displayName || user.username || ''; // displayName can be empty and username can be null - var nameParts = displayName.split(' '); - var firstName = nameParts[0]; - var lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists + const displayName = user.displayName || user.username || ''; // displayName can be empty and username can be null + const nameParts = displayName.split(' '); + const firstName = nameParts[0]; + const lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists - var obj = { - dn: dn.toString(), - attributes: { - objectclass: ['user', 'inetorgperson', 'person' ], - objectcategory: 'person', - cn: user.id, - uid: user.id, - entryuuid: user.id, // to support OpenLDAP clients - mail: user.email, - mailAlternateAddress: user.fallbackEmail, - displayname: displayName, - givenName: firstName, - username: user.username, - samaccountname: user.username, // to support ActiveDirectory clients - memberof: memberof - } - }; - - // http://www.zytrax.com/books/ldap/ape/core-schema.html#sn has 'name' as SUP which is a DirectoryString - // which is required to have atleast one character if present - if (lastName.length !== 0) obj.attributes.sn = lastName; - - // ensure all filter values are also lowercase - var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); - if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); - - if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { - results.push(obj); + const obj = { + dn: dn.toString(), + attributes: { + objectclass: ['user', 'inetorgperson', 'person' ], + objectcategory: 'person', + cn: user.id, + uid: user.id, + entryuuid: user.id, // to support OpenLDAP clients + mail: user.email, + mailAlternateAddress: user.fallbackEmail, + displayname: displayName, + givenName: firstName, + username: user.username, + samaccountname: user.username, // to support ActiveDirectory clients + memberof: memberof } - }); + }; - finalSend(results, req, res, next); + // http://www.zytrax.com/books/ldap/ape/core-schema.html#sn has 'name' as SUP which is a DirectoryString + // which is required to have atleast one character if present + if (lastName.length !== 0) obj.attributes.sn = lastName; + + // ensure all filter values are also lowercase + const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); + if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); + + if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { + results.push(obj); + } }); + + finalSend(results, req, res, next); } -function groupSearch(req, res, next) { +async function groupSearch(req, res, next) { debug('group search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); - getUsersWithAccessToApp(req, function (error, result) { - if (error) return next(error); + const [error, result] = await safe(getUsersWithAccessToApp(req)); + if (error) return next(new ldap.OperationsError(error.toString())); - var results = []; + const results = []; - var groups = [{ - name: 'users', - admin: false - }, { - name: 'admins', - admin: true - }]; + const groups = [{ + name: 'users', + admin: false + }, { + name: 'admins', + admin: true + }]; - groups.forEach(function (group) { - var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron'); - var members = group.admin ? result.filter(function (user) { return users.compareRoles(user.role, users.ROLE_ADMIN) >= 0; }) : result; + groups.forEach(function (group) { + const dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron'); + const members = group.admin ? result.filter(function (user) { return users.compareRoles(user.role, users.ROLE_ADMIN) >= 0; }) : result; - var obj = { - dn: dn.toString(), - attributes: { - objectclass: ['group'], - cn: group.name, - memberuid: members.map(function(entry) { return entry.id; }) - } - }; - - // ensure all filter values are also lowercase - var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); - if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); - - if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { - results.push(obj); + const obj = { + dn: dn.toString(), + attributes: { + objectclass: ['group'], + cn: group.name, + memberuid: members.map(function(entry) { return entry.id; }) } - }); + }; - finalSend(results, req, res, next); + // ensure all filter values are also lowercase + const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); + if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); + + if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { + results.push(obj); + } }); + + finalSend(results, req, res, next); } -function groupUsersCompare(req, res, next) { +async function groupUsersCompare(req, res, next) { debug('group users compare: dn %s, attribute %s, value %s (from %s)', req.dn.toString(), req.attribute, req.value, req.connection.ldap.id); - getUsersWithAccessToApp(req, function (error, result) { - if (error) return next(error); + const [error, result] = await safe(getUsersWithAccessToApp(req)); + if (error) return next(new ldap.OperationsError(error.toString())); - // we only support memberuid here, if we add new group attributes later add them here - if (req.attribute === 'memberuid') { - var found = result.find(function (u) { return u.id === req.value; }); - if (found) return res.end(true); - } + // we only support memberuid here, if we add new group attributes later add them here + if (req.attribute === 'memberuid') { + const found = result.find(function (u) { return u.id === req.value; }); + if (found) return res.end(true); + } - res.end(false); - }); + res.end(false); } -function groupAdminsCompare(req, res, next) { +async function groupAdminsCompare(req, res, next) { debug('group admins compare: dn %s, attribute %s, value %s (from %s)', req.dn.toString(), req.attribute, req.value, req.connection.ldap.id); - getUsersWithAccessToApp(req, function (error, result) { - if (error) return next(error); + const [error, result] = await safe(getUsersWithAccessToApp(req)); + if (error) return next(new ldap.OperationsError(error.toString())); - // we only support memberuid here, if we add new group attributes later add them here - if (req.attribute === 'memberuid') { - var user = result.find(function (u) { return u.id === req.value; }); - if (user && users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) return res.end(true); - } + // we only support memberuid here, if we add new group attributes later add them here + if (req.attribute === 'memberuid') { + var user = result.find(function (u) { return u.id === req.value; }); + if (user && users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) return res.end(true); + } - res.end(false); - }); + res.end(false); } async function mailboxSearch(req, res, next) { @@ -483,20 +467,17 @@ async function authenticateUser(req, res, next) { next(); } -function authorizeUserForApp(req, res, next) { +async function authorizeUserForApp(req, res, next) { assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.app, 'object'); - apps.hasAccessTo(req.app, req.user, async function (error, hasAccess) { - if (error) return next(new ldap.OperationsError(error.toString())); + const hasAccess = apps.hasAccessTo(req.app, req.user); + // we return no such object, to avoid leakage of a users existence + if (!hasAccess) return next(new ldap.NoSuchObjectError(req.dn.toString())); - // we return no such object, to avoid leakage of a users existence - if (!hasAccess) return next(new ldap.NoSuchObjectError(req.dn.toString())); + await eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) }); - eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) }); - - res.end(); - }); + res.end(); } async function verifyMailboxPassword(mailbox, password) { @@ -549,7 +530,7 @@ async function authenticateUserMailbox(req, res, next) { res.end(); } -function authenticateSftp(req, res, next) { +async function authenticateSftp(req, res, next) { debug('sftp auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); @@ -558,16 +539,15 @@ function authenticateSftp(req, res, next) { const parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); - apps.getByFqdn(parts[1], async function (error, app) { - if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString())); + let [error, app] = await safe(apps.getByFqdn(parts[1])); + if (error || !app) return next(new ldap.InvalidCredentialsError(req.dn.toString())); - [error] = await safe(users.verifyWithUsername(parts[0], req.credentials, app.id)); - if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString())); + [error] = await safe(users.verifyWithUsername(parts[0], req.credentials, app.id)); + if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString())); - debug('sftp auth: success'); + debug('sftp auth: success'); - res.end(); - }); + res.end(); } function loadSftpConfig(req, res, next) { @@ -580,52 +560,49 @@ function loadSftpConfig(req, res, next) { }); } -function userSearchSftp(req, res, next) { +async function userSearchSftp(req, res, next) { debug('sftp user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); if (req.filter.attribute !== 'username' || !req.filter.value) return next(new ldap.NoSuchObjectError(req.dn.toString())); - var parts = req.filter.value.split('@'); + const parts = req.filter.value.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); - var username = parts[0]; - var appFqdn = parts[1]; + const username = parts[0]; + const appFqdn = parts[1]; - apps.getByFqdn(appFqdn, async function (error, app) { - if (error) return next(new ldap.OperationsError(error.toString())); + const [error, app] = await safe(apps.getByFqdn(appFqdn)); + if (error) return next(new ldap.OperationsError(error.toString())); - // only allow apps which specify "ftp" support in the localstorage addon - if (!safe.query(app.manifest.addons, 'localstorage.ftp.uid')) return next(new ldap.UnavailableError('Not supported')); - if (typeof app.manifest.addons.localstorage.ftp.uid !== 'number') return next(new ldap.UnavailableError('Bad uid, must be a number')); + // only allow apps which specify "ftp" support in the localstorage addon + if (!safe.query(app.manifest.addons, 'localstorage.ftp.uid')) return next(new ldap.UnavailableError('Not supported')); + if (typeof app.manifest.addons.localstorage.ftp.uid !== 'number') return next(new ldap.UnavailableError('Bad uid, must be a number')); - const uidNumber = app.manifest.addons.localstorage.ftp.uid; + const uidNumber = app.manifest.addons.localstorage.ftp.uid; - const [userGetError, user] = await safe(users.getByUsername(username)); - if (userGetError) return next(new ldap.OperationsError(userGetError.toString())); - if (!user) return next(new ldap.OperationsError('Invalid username')); + const [userGetError, user] = await safe(users.getByUsername(username)); + if (userGetError) return next(new ldap.OperationsError(userGetError.toString())); + if (!user) return next(new ldap.OperationsError('Invalid username')); - if (req.requireAdmin && users.compareRoles(user.role, users.ROLE_ADMIN) < 0) return next(new ldap.InsufficientAccessRightsError('Insufficient previleges')); + if (req.requireAdmin && users.compareRoles(user.role, users.ROLE_ADMIN) < 0) return next(new ldap.InsufficientAccessRightsError('Insufficient previleges')); - apps.hasAccessTo(app, user, function (error, hasAccess) { - if (error) return next(new ldap.OperationsError(error.toString())); - if (!hasAccess) return next(new ldap.InsufficientAccessRightsError('Not authorized')); + const hasAccess = apps.hasAccessTo(app, user); + if (!hasAccess) return next(new ldap.InsufficientAccessRightsError('Not authorized')); - const obj = { - dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(), - attributes: { - homeDirectory: app.dataDir ? `/mnt/${app.id}` : `/mnt/appsdata/${app.id}/data`, - objectclass: ['user'], - objectcategory: 'person', - cn: user.id, - uid: `${username}@${appFqdn}`, // for bind after search - uidNumber: uidNumber, // unix uid for ftp access - gidNumber: uidNumber // unix gid for ftp access - } - }; + const obj = { + dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(), + attributes: { + homeDirectory: app.dataDir ? `/mnt/${app.id}` : `/mnt/appsdata/${app.id}/data`, + objectclass: ['user'], + objectcategory: 'person', + cn: user.id, + uid: `${username}@${appFqdn}`, // for bind after search + uidNumber: uidNumber, // unix uid for ftp access + gidNumber: uidNumber // unix gid for ftp access + } + }; - finalSend([ obj ], req, res, next); - }); - }); + finalSend([ obj ], req, res, next); } async function verifyAppMailboxPassword(addonId, username, password) { @@ -684,7 +661,7 @@ async function authenticateMailAddon(req, res, next) { function start(callback) { assert.strictEqual(typeof callback, 'function'); - var logger = { + const logger = { trace: NOOP, debug: NOOP, info: debug, diff --git a/src/platform.js b/src/platform.js index 8eb164edf..18bb415de 100644 --- a/src/platform.js +++ b/src/platform.js @@ -119,18 +119,17 @@ function removeAllContainers(callback) { ], callback); } -function markApps(existingInfra, options, callback) { +async function markApps(existingInfra, options) { assert.strictEqual(typeof existingInfra, 'object'); assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); if (existingInfra.version === 'none') { // cloudron is being restored from backup debug('markApps: restoring installed apps'); - apps.restoreInstalledApps(options, callback); + await apps.restoreInstalledApps(options); } else if (existingInfra.version !== infra.version) { debug('markApps: reconfiguring installed apps'); reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start - apps.configureInstalledApps(callback); + await apps.configureInstalledApps(); } else { let changedAddons = []; if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) changedAddons.push('mysql'); @@ -141,10 +140,9 @@ function markApps(existingInfra, options, callback) { if (changedAddons.length) { // restart apps if docker image changes since the IP changes and any "persistent" connections fail debug(`markApps: changedAddons: ${JSON.stringify(changedAddons)}`); - apps.restartAppsUsingAddons(changedAddons, callback); + await apps.restartAppsUsingAddons(changedAddons); } else { debug('markApps: apps are already uptodate'); - callback(); } } } diff --git a/src/proxyauth.js b/src/proxyauth.js index eabf14425..272446d8d 100644 --- a/src/proxyauth.js +++ b/src/proxyauth.js @@ -49,58 +49,56 @@ function jwtVerify(req, res, next) { }); } -function basicAuthVerify(req, res, next) { +async function basicAuthVerify(req, res, next) { const appId = req.headers['x-app-id'] || ''; const credentials = basicAuth(req); if (!appId || !credentials) return next(); - apps.get(appId, async function (error, app) { - if (error) return next(new HttpError(503, error.message)); + const [error, app] = await safe(apps.get(appId)); + if (error) return next(new HttpError(503, error.message)); - if (!app.manifest.addons.proxyAuth.basicAuth) return next(); + if (!app.manifest.addons.proxyAuth.basicAuth) return next(); - const verifyFunc = credentials.name.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername; - const [verifyError, user] = await safe(verifyFunc(credentials.name, credentials.pass, appId)); - if (verifyError) return next(new HttpError(403, 'Invalid username or password' )); + const verifyFunc = credentials.name.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername; + const [verifyError, user] = await safe(verifyFunc(credentials.name, credentials.pass, appId)); + if (verifyError) return next(new HttpError(403, 'Invalid username or password' )); - req.user = user; - next(); - }); + req.user = user; + next(); } -function loginPage(req, res, next) { +async function loginPage(req, res, next) { const appId = req.headers['x-app-id'] || ''; if (!appId) return next(new HttpError(503, 'Nginx misconfiguration')); - util.callbackify(translation.getTranslations)(function (error, translationAssets) { - if (error) return next(new HttpError(500, 'No translation found')); + const [error, translationAssets] = await safe(translation.getTranslations()); + if (error) return next(new HttpError(500, 'No translation found')); - const raw = safe.fs.readFileSync(path.join(paths.DASHBOARD_DIR, 'templates/proxyauth-login.ejs'), 'utf8'); - if (raw === null) return next(new HttpError(500, 'Login template not found')); + const raw = safe.fs.readFileSync(path.join(paths.DASHBOARD_DIR, 'templates/proxyauth-login.ejs'), 'utf8'); + if (raw === null) return next(new HttpError(500, 'Login template not found')); - const translatedContent = translation.translate(raw, translationAssets.translations || {}, translationAssets.fallback || {}); - var finalContent = ''; + const translatedContent = translation.translate(raw, translationAssets.translations || {}, translationAssets.fallback || {}); + let finalContent = ''; - apps.get(appId, function (error, app) { - if (error) return next(new HttpError(503, error.message)); + const [getError, app] = await safe(apps.get(appId)); + if (getError) return next(new HttpError(503, getError.message)); - const title = app.label || app.manifest.title; + const title = app.label || app.manifest.title; - apps.getIcon(app, {}, function (error, iconBuffer) { - const icon = 'data:image/png;base64,' + iconBuffer.toString('base64'); + const [iconError, iconBuffer] = await safe(apps.getIcon(app, {})); + if (iconError || !iconBuffer) return next(new HttpError(500, 'Icon rendering error')); - try { - finalContent = ejs.render(translatedContent, { title, icon }); - } catch (e) { - debug('Error rendering proxyauth-login.ejs', e); - return next(new HttpError(500, 'Login template error')); - } + const icon = 'data:image/png;base64,' + iconBuffer.toString('base64'); - res.set('Content-Type', 'text/html'); - return res.send(finalContent); - }); - }); - }); + try { + finalContent = ejs.render(translatedContent, { title, icon }); + } catch (e) { + debug('Error rendering proxyauth-login.ejs', e); + return next(new HttpError(500, 'Login template error')); + } + + res.set('Content-Type', 'text/html'); + return res.send(finalContent); } // someday this can be more sophisticated and check for a real browser @@ -162,43 +160,38 @@ async function passwordAuth(req, res, next) { next(); } -function authorize(req, res, next) { +async function authorize(req, res, next) { const appId = req.headers['x-app-id'] || ''; if (!appId) return next(new HttpError(503, 'Nginx misconfiguration')); - apps.get(appId, function (error, app) { - if (error) return next(new HttpError(403, 'No such app' )); + const [error, app] = await safe(apps.get(appId)); + if (error) return next(new HttpError(403, 'No such app' )); - apps.hasAccessTo(app, req.user, function (error, hasAccess) { - if (error) return next(new HttpError(403, 'Forbidden' )); - if (!hasAccess) return next(new HttpError(403, 'Forbidden' )); + if (!apps.hasAccessTo(app, req.user)) return next(new HttpError(403, 'Forbidden' )); - const token = jwt.sign({ user: users.removePrivateFields(req.user) }, TOKEN_SECRET, { expiresIn: `${constants.DEFAULT_TOKEN_EXPIRATION_DAYS}d` }); + const token = jwt.sign({ user: users.removePrivateFields(req.user) }, TOKEN_SECRET, { expiresIn: `${constants.DEFAULT_TOKEN_EXPIRATION_DAYS}d` }); - res.cookie('authToken', token, { - httpOnly: true, - maxAge: constants.DEFAULT_TOKEN_EXPIRATION_MSECS, - secure: true - }); - - res.redirect(302, '/'); - }); + res.cookie('authToken', token, { + httpOnly: true, + maxAge: constants.DEFAULT_TOKEN_EXPIRATION_MSECS, + secure: true }); + + res.redirect(302, '/'); } -function logoutPage(req, res, next) { +async function logoutPage(req, res, next) { const appId = req.headers['x-app-id'] || ''; if (!appId) return next(new HttpError(503, 'Nginx misconfiguration')); - apps.get(appId, function (error, app) { - if (error) return next(new HttpError(503, error.message)); + const [error, app] = await safe(apps.get(appId)); + if (error) return next(new HttpError(503, error.message)); - res.clearCookie('authToken'); + res.clearCookie('authToken'); - // when we have no path, redirect to the login page. we cannot redirect to '/' because browsers will immediately serve up the cached page - // if a path is set, we can assume '/' is a public page - res.redirect(302, app.manifest.addons.proxyAuth.path ? '/' : '/login'); - }); + // when we have no path, redirect to the login page. we cannot redirect to '/' because browsers will immediately serve up the cached page + // if a path is set, we can assume '/' is a public page + res.redirect(302, app.manifest.addons.proxyAuth.path ? '/' : '/login'); } function logout(req, res, next) { diff --git a/src/reverseproxy.js b/src/reverseproxy.js index b7c2e8af4..3189a771c 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -629,7 +629,7 @@ function renewCerts(options, auditSource, progressCallback, callback) { assert.strictEqual(typeof progressCallback, 'function'); assert.strictEqual(typeof callback, 'function'); - apps.getAll(function (error, allApps) { + util.callbackify(apps.list)(function (error, allApps) { if (error) return callback(error); let appDomains = []; diff --git a/src/routes/apps.js b/src/routes/apps.js index f4bf22f94..ca8c15a93 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -2,7 +2,7 @@ exports = module.exports = { getApp, - getApps, + listByUser, getAppIcon, install, uninstall, @@ -59,16 +59,15 @@ var apps = require('../apps.js'), users = require('../users.js'), WebSocket = require('ws'); -function load(req, res, next) { +async function load(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); - apps.get(req.params.id, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await apps.get(req.params.id); + if (error) return next(BoxError.toHttpError(error)); - req.resource = result; + req.resource = result; - next(); - }); + next(); } function getApp(req, res, next) { @@ -77,29 +76,27 @@ function getApp(req, res, next) { next(new HttpSuccess(200, apps.removeInternalFields(req.resource))); } -function getApps(req, res, next) { +async function listByUser(req, res, next) { assert.strictEqual(typeof req.user, 'object'); - apps.getAllByUser(req.user, function (error, allApps) { - if (error) return next(BoxError.toHttpError(error)); + let [error, result] = await safe(apps.listByUser(req.user)); + if (error) return next(BoxError.toHttpError(error)); - allApps = allApps.map(apps.removeRestrictedFields); + result = result.map(apps.removeRestrictedFields); - next(new HttpSuccess(200, { apps: allApps })); - }); + next(new HttpSuccess(200, { apps: result })); } -function getAppIcon(req, res, next) { +async function getAppIcon(req, res, next) { assert.strictEqual(typeof req.resource, 'object'); - apps.getIcon(req.resource, { original: req.query.original }, function (error, icon) { - if (error) return next(BoxError.toHttpError(error)); + const [error, icon] = await safe(apps.getIcon(req.resource, { original: req.query.original })); + if (error) return next(BoxError.toHttpError(error)); - res.send(icon); - }); + res.send(icon); } -function install(req, res, next) { +async function install(req, res, next) { assert.strictEqual(typeof req.body, 'object'); const data = req.body; @@ -147,85 +144,79 @@ function install(req, res, next) { if ('skipDnsSetup' in req.body && typeof req.body.skipDnsSetup !== 'boolean') return next(new HttpError(400, 'skipDnsSetup must be boolean')); if ('enableMailbox' in req.body && typeof req.body.enableMailbox !== 'boolean') return next(new HttpError(400, 'enableMailbox must be boolean')); - apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) { - if (error) return next(BoxError.toHttpError(error)); + let [error, result] = await safe(apps.downloadManifest(data.appStoreId, data.manifest)); + if (error) return next(BoxError.toHttpError(error)); - if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to install app with docker addon')); + if (safe.query(result.manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to install app with docker addon')); - data.appStoreId = appStoreId; - data.manifest = manifest; - apps.install(data, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + data.appStoreId = result.appStoreId; + data.manifest = result.manifest; - next(new HttpSuccess(202, { id: result.id, taskId: result.taskId })); - }); - }); + [error, result] = await safe(apps.install(data, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(202, { id: result.id, taskId: result.taskId })); } -function setAccessRestriction(req, res, next) { +async function setAccessRestriction(req, res, next) { assert.strictEqual(typeof req.body, 'object'); 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.resource, req.body.accessRestriction, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(apps.setAccessRestriction(req.resource, req.body.accessRestriction, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, {})); - }); + next(new HttpSuccess(200, {})); } -function setLabel(req, res, next) { +async function setLabel(req, res, next) { assert.strictEqual(typeof req.body, 'object'); 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.resource, req.body.label, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(apps.setLabel(req.resource, req.body.label, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, {})); - }); + next(new HttpSuccess(200, {})); } -function setTags(req, res, next) { +async function setTags(req, res, next) { assert.strictEqual(typeof req.body, 'object'); 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.resource, req.body.tags, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(apps.setTags(req.resource, req.body.tags, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, {})); - }); + next(new HttpSuccess(200, {})); } -function setIcon(req, res, next) { +async function setIcon(req, res, next) { assert.strictEqual(typeof req.body, 'object'); 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.resource, req.body.icon || null /* empty string means null */, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(apps.setIcon(req.resource, req.body.icon || null /* empty string means null */, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, {})); - }); + next(new HttpSuccess(200, {})); } -function setMemoryLimit(req, res, next) { +async function setMemoryLimit(req, res, next) { assert.strictEqual(typeof req.body, 'object'); 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.resource, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.setMemoryLimit(req.resource, req.body.memoryLimit, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } function setCpuShares(req, res, next) { @@ -241,30 +232,28 @@ function setCpuShares(req, res, next) { }); } -function setAutomaticBackup(req, res, next) { +async function setAutomaticBackup(req, res, next) { assert.strictEqual(typeof req.body, 'object'); 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.resource, req.body.enable, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(apps.setAutomaticBackup(req.resource, req.body.enable, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, {})); - }); + next(new HttpSuccess(200, {})); } -function setAutomaticUpdate(req, res, next) { +async function setAutomaticUpdate(req, res, next) { assert.strictEqual(typeof req.body, 'object'); 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.resource, req.body.enable, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(apps.setAutomaticUpdate(req.resource, req.body.enable, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, {})); - }); + next(new HttpSuccess(200, {})); } async function setReverseProxyConfig(req, res, next) { @@ -300,34 +289,32 @@ async function setCertificate(req, res, next) { next(new HttpSuccess(200, {})); } -function setEnvironment(req, res, next) { +async function setEnvironment(req, res, next) { assert.strictEqual(typeof req.body, 'object'); 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.resource, req.body.env, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.setEnvironment(req.resource, req.body.env, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } -function setDebugMode(req, res, next) { +async function setDebugMode(req, res, next) { assert.strictEqual(typeof req.body, 'object'); 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.resource, req.body.debugMode, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.setDebugMode(req.resource, req.body.debugMode, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } -function setMailbox(req, res, next) { +async function setMailbox(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.resource, 'object'); @@ -337,14 +324,13 @@ function setMailbox(req, res, next) { if (typeof req.body.mailboxDomain !== 'string') return next(new HttpError(400, 'mailboxDomain must be a string')); } - apps.setMailbox(req.resource, req.body, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.setMailbox(req.resource, req.body, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } -function setLocation(req, res, next) { +async function setLocation(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.resource, 'object'); @@ -367,27 +353,25 @@ function setLocation(req, res, next) { if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean')); if ('skipDnsSetup' in req.body && typeof req.body.skipDnsSetup !== 'boolean') return next(new HttpError(400, 'skipDnsSetup must be boolean')); - apps.setLocation(req.resource, req.body, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.setLocation(req.resource, req.body, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } -function setDataDir(req, res, next) { +async function setDataDir(req, res, next) { assert.strictEqual(typeof req.body, 'object'); 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.resource, req.body.dataDir, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.setDataDir(req.resource, req.body.dataDir, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } -function repair(req, res, next) { +async function repair(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.resource, 'object'); @@ -403,11 +387,10 @@ function repair(req, res, next) { if (!data.dockerImage || typeof data.dockerImage !== 'string') return next(new HttpError(400, 'dockerImage must be a string')); } - apps.repair(req.resource, data, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.repair(req.resource, data, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } async function restore(req, res, next) { @@ -424,11 +407,11 @@ async function restore(req, res, next) { next(new HttpSuccess(202, { taskId: result.taskId })); } -function importApp(req, res, next) { +async function importApp(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.resource, 'object'); - var data = req.body; + const data = req.body; if ('backupId' in data) { // if not provided, we import in-place if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string')); @@ -448,25 +431,23 @@ function importApp(req, res, next) { } } - apps.importApp(req.resource, data, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.importApp(req.resource, data, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } -function exportApp(req, res, next) { +async function exportApp(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.resource, 'object'); - apps.exportApp(req.resource, {}, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.exportApp(req.resource, {}, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } -function clone(req, res, next) { +async function clone(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.resource, 'object'); @@ -480,68 +461,62 @@ function clone(req, res, next) { if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean')); if ('skipDnsSetup' in req.body && typeof req.body.skipDnsSetup !== 'boolean') return next(new HttpError(400, 'skipDnsSetup must be boolean')); - apps.clone(req.resource, data, req.user, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.clone(req.resource, data, req.user, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, { id: result.id, taskId: result.taskId })); - }); + next(new HttpSuccess(201, { id: result.id, taskId: result.taskId })); } -function backup(req, res, next) { +async function backup(req, res, next) { assert.strictEqual(typeof req.resource, 'object'); - apps.backup(req.resource, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.backup(req.resource)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } -function uninstall(req, res, next) { +async function uninstall(req, res, next) { assert.strictEqual(typeof req.resource, 'object'); - apps.uninstall(req.resource, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.uninstall(req.resource, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } -function start(req, res, next) { +async function start(req, res, next) { assert.strictEqual(typeof req.resource, 'object'); - apps.start(req.resource, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.start(req.resource, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } -function stop(req, res, next) { +async function stop(req, res, next) { assert.strictEqual(typeof req.resource, 'object'); - apps.stop(req.resource, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.stop(req.resource, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } -function restart(req, res, next) { +async function restart(req, res, next) { assert.strictEqual(typeof req.resource, 'object'); - apps.restart(req.resource, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.restart(req.resource, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } -function update(req, res, next) { +async function update(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.resource, 'object'); - var data = req.body; + const data = req.body; // atleast one if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object')); @@ -551,19 +526,19 @@ function update(req, res, next) { if ('skipBackup' in data && typeof data.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean')); if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean')); - apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) { - if (error) return next(BoxError.toHttpError(error)); + let [error, result] = await safe(apps.downloadManifest(data.appStoreId, data.manifest)); + if (error) return next(BoxError.toHttpError(error)); - if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to update app with docker addon')); + const { appStoreId, manifest } = result; - data.appStoreId = appStoreId; - data.manifest = manifest; - apps.update(req.resource, data, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to update app with docker addon')); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); - }); + data.appStoreId = appStoreId; + data.manifest = manifest; + [error, result] = await safe(apps.updateApp(req.resource, data, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(202, { taskId: result.taskId })); } // this route is for streaming logs @@ -789,7 +764,7 @@ function downloadFile(req, res, next) { }); } -function setMounts(req, res, next) { +async function setMounts(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.resource, 'object'); @@ -800,9 +775,8 @@ function setMounts(req, res, next) { if (typeof m.readOnly !== 'boolean') return next(new HttpError(400, 'readOnly must be a boolean')); } - apps.setMounts(req.resource, req.body.mounts, auditSource.fromRequest(req), function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(apps.setMounts(req.resource, req.body.mounts, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); - }); + next(new HttpSuccess(202, { taskId: result.taskId })); } diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index 6726f64c0..215454de7 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -275,14 +275,13 @@ async function updateDashboardDomain(req, res, next) { next(new HttpSuccess(204, {})); } -function prepareDashboardDomain(req, res, next) { +async function prepareDashboardDomain(req, res, next) { if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string')); - cloudron.prepareDashboardDomain(req.body.domain, auditSource.fromRequest(req), function (error, taskId) { - if (error) return next(BoxError.toHttpError(error)); + const [error, taskId] = await safe(cloudron.prepareDashboardDomain(req.body.domain, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId })); - }); + next(new HttpSuccess(202, { taskId })); } async function renewCerts(req, res, next) { diff --git a/src/routes/services.js b/src/routes/services.js index c5218dce6..6dbd927d0 100644 --- a/src/routes/services.js +++ b/src/routes/services.js @@ -14,14 +14,14 @@ const assert = require('assert'), BoxError = require('../boxerror.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, + safe = require('safetydance'), services = require('../services.js'); -function getAll(req, res, next) { - services.getServiceIds(function (error, result) { - if (error) return next(BoxError.toHttpError(error)); +async function getAll(req, res, next) { + const [error, result] = await safe(services.getServiceIds()); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { services: result })); - }); + next(new HttpSuccess(200, { services: result })); } function get(req, res, next) { diff --git a/src/routes/test/common.js b/src/routes/test/common.js index 11b43f46b..efa8fd6f0 100644 --- a/src/routes/test/common.js +++ b/src/routes/test/common.js @@ -12,7 +12,8 @@ const async = require('async'), settings = require('../../settings.js'), support = require('../../support.js'), superagent = require('superagent'), - tokens = require('../../tokens.js'); + tokens = require('../../tokens.js'), + util = require('util'); exports = module.exports = { setup, @@ -49,7 +50,7 @@ exports = module.exports = { function setupServer(done) { async.series([ server.start.bind(null), - database._clear.bind(null), + database._clear, settings._setApiServerOrigin.bind(null, exports.mockApiServerOrigin), ], done); } @@ -109,12 +110,9 @@ function setup(done) { ], done); } -function cleanup(done) { - database._clear(function (error) { - expect(!error).to.be.ok(); - - server.stop(done); - }); +async function cleanup() { + await database._clear(); + await util.promisify(server.stop)(); } function clearMailQueue() { diff --git a/src/scheduler.js b/src/scheduler.js index 9b164ef42..b91824a8e 100644 --- a/src/scheduler.js +++ b/src/scheduler.js @@ -14,6 +14,7 @@ const apps = require('./apps.js'), CronJob = require('cron').CronJob, debug = require('debug')('box:scheduler'), docker = require('./docker.js'), + util = require('util'), _ = require('underscore'); // appId -> { containerId, schedulerConfig (manifest), cronjobs } @@ -41,8 +42,9 @@ function runTask(appId, taskName, callback) { if (gSuspendedAppIds.has(appId)) return callback(); - apps.get(appId, function (error, app) { + util.callbackify(apps.get)(appId, function (error, app) { if (error) return callback(error); + if (!app) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found')); if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING || app.health !== apps.HEALTH_HEALTHY) return callback(); @@ -121,7 +123,7 @@ function stopJobs(appId, appState, callback) { function sync() { if (constants.TEST) return; - apps.getAll(function (error, allApps) { + util.callbackify(apps.list)(function (error, allApps) { if (error) return debug(`sync: error getting app list. ${error.message}`); var allAppIds = allApps.map(function (app) { return app.id; }); diff --git a/src/server.js b/src/server.js index 4dc07ef2c..96e6ccbe6 100644 --- a/src/server.js +++ b/src/server.js @@ -197,7 +197,7 @@ function initializeExpressSync() { // app routes router.post('/api/v1/apps/install', json, token, authorizeAdmin, routes.apps.install); - router.get ('/api/v1/apps', token, routes.apps.getApps); + router.get ('/api/v1/apps', token, routes.apps.listByUser); 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/:id/uninstall', json, token, authorizeAdmin, routes.apps.load, routes.apps.uninstall); diff --git a/src/services.js b/src/services.js index 4f2da80e0..e07656086 100644 --- a/src/services.js +++ b/src/services.js @@ -32,7 +32,6 @@ exports = module.exports = { }; const addonConfigs = require('./addonconfigs.js'), - appdb = require('./appdb.js'), apps = require('./apps.js'), assert = require('assert'), async = require('async'), @@ -329,20 +328,15 @@ function containerStatus(containerName, tokenEnvName, callback) { }); } -function getServiceIds(callback) { - assert.strictEqual(typeof callback, 'function'); - +async function getServiceIds() { let serviceIds = Object.keys(SERVICES); - appdb.getAll(function (error, apps) { - if (error) return callback(error); + const result = await apps.list(); + for (let app of result) { + if (app.manifest.addons && app.manifest.addons['redis']) serviceIds.push(`redis:${app.id}`); + } - for (let app of apps) { - if (app.manifest.addons && app.manifest.addons['redis']) serviceIds.push(`redis:${app.id}`); - } - - callback(null, serviceIds); - }); + return serviceIds; } function getServiceConfig(id, callback) { @@ -360,8 +354,9 @@ function getServiceConfig(id, callback) { return; } - appdb.get(instance, function (error, app) { + util.callbackify(apps.get)(instance, function (error, app) { if (error) return callback(error); + if (!app) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found')); callback(null, app.servicesConfig[name] || {}); }); @@ -428,17 +423,17 @@ function configureService(id, data, callback) { if (instance) { if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND)); - apps.get(instance, function (error, app) { + util.callbackify(apps.get)(instance, async function (error, app) { if (error) return callback(error); + if (!app) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found')); const servicesConfig = app.servicesConfig; servicesConfig[name] = data; - appdb.update(instance, { servicesConfig }, function (error) { - if (error) return callback(error); + const [updateError] = await safe(apps.update(instance, { servicesConfig })); + if (updateError) return callback(updateError); - applyServiceConfig(id, data, callback); - }); + applyServiceConfig(id, data, callback); }); } else if (SERVICES[name]) { settingsGetServicesConfig(function (error, servicesConfig) { @@ -742,7 +737,7 @@ function importDatabase(addon, callback) { debug(`importDatabase: Importing ${addon}`); - appdb.getAll(function (error, allApps) { + util.callbackify(apps.list)(function (error, allApps) { if (error) return callback(error); async.eachSeries(allApps, function iterator (app, iteratorCallback) { @@ -750,13 +745,14 @@ function importDatabase(addon, callback) { debug(`importDatabase: Importing addon ${addon} of app ${app.id}`); - importAppDatabase(app, addon, function (error) { + importAppDatabase(app, addon, async function (error) { if (!error) return iteratorCallback(); debug(`importDatabase: Error importing ${addon} of app ${app.id}. Marking as errored`, error); // FIXME: there is no way to 'repair' if we are here. we need to make a separate apptask that re-imports db // not clear, if repair workflow should be part of addon or per-app - appdb.update(app.id, { installationState: apps.ISTATE_ERROR, error: { message: error.message } }, iteratorCallback); + const [updateError] = await safe(apps.update(app.id, { installationState: apps.ISTATE_ERROR, error: { message: error.message } })); + iteratorCallback(updateError); }); }, function (error) { safe.fs.unlinkSync(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`)); // clean up for future migrations @@ -777,7 +773,7 @@ function exportDatabase(addon, callback) { return callback(null); } - appdb.getAll(function (error, allApps) { + util.callbackify(apps.list)(function (error, allApps) { if (error) return callback(error); async.eachSeries(allApps, function iterator (app, iteratorCallback) { @@ -1878,7 +1874,7 @@ function startRedis(existingInfra, callback) { const tag = infra.images.redis.tag; const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.redis.tag, tag); - apps.getAll(function (error, allApps) { + util.callbackify(apps.list)(function (error, allApps) { if (error) return callback(error); async.eachSeries(allApps, function iterator (app, iteratorCallback) { diff --git a/src/sftp.js b/src/sftp.js index 1cdcd5852..00f26418c 100644 --- a/src/sftp.js +++ b/src/sftp.js @@ -18,6 +18,7 @@ const apps = require('./apps.js'), safe = require('safetydance'), shell = require('./shell.js'), system = require('./system.js'), + util = require('util'), volumes = require('./volumes.js'); function rebuild(serviceConfig, options, callback) { @@ -41,7 +42,7 @@ function rebuild(serviceConfig, options, callback) { dataDirs.push({ hostDir: resolvedAppDataDir, mountDir: '/mnt/appsdata' }); - apps.getAll(async function (error, result) { + util.callbackify(apps.list)(async function (error, result) { if (error) return callback(error); result.forEach(function (app) { diff --git a/src/system.js b/src/system.js index cf0cc587a..30a9b9004 100644 --- a/src/system.js +++ b/src/system.js @@ -42,22 +42,17 @@ async function getAppDisks(appsDataDisk) { let appDisks = {}; - return new Promise((resolve, reject) => { - apps.getAll(async function (error, allApps) { - if (error) return reject(error); + const allApps = await apps.list(); + for (const app of allApps) { + if (!app.dataDir) { + appDisks[app.id] = appsDataDisk; + } else { + const [error, result] = await safe(df.file(app.dataDir)); + appDisks[app.id] = error ? appsDataDisk : result.filesystem; // ignore any errors + } + } - for (const app of allApps) { - if (!app.dataDir) { - appDisks[app.id] = appsDataDisk; - } else { - const [error, result] = await safe(df.file(app.dataDir)); - appDisks[app.id] = error ? appsDataDisk : result.filesystem; // ignore any errors - } - } - - resolve(appDisks); - }); - }); + return appDisks; } async function getBackupDisk() { diff --git a/src/taskworker.js b/src/taskworker.js index fe17579bd..6ba06b91c 100755 --- a/src/taskworker.js +++ b/src/taskworker.js @@ -106,7 +106,7 @@ async.series([ try { if (util.types.isAsyncFunction(TASKS[task.type])) { // can also use fn[Symbol.toStringTag] const [error, result] = await safe(TASKS[task.type].apply(null, task.args.concat(progressCallback))); - resultCallback(error, result); + await resultCallback(error, result); } else { TASKS[task.type].apply(null, task.args.concat(progressCallback).concat(resultCallback)); } diff --git a/src/test/apps-test.js b/src/test/apps-test.js index 2be3c59a9..19dde3f73 100644 --- a/src/test/apps-test.js +++ b/src/test/apps-test.js @@ -5,151 +5,17 @@ 'use strict'; -const appdb = require('../appdb.js'), - apps = require('../apps.js'), - async = require('async'), - constants = require('../constants.js'), +const apps = require('../apps.js'), BoxError = require('../boxerror.js'), common = require('./common.js'), - domains = require('../domains.js'), expect = require('expect.js'), - hat = require('../hat.js'); - -let AUDIT_SOURCE = { ip: '1.2.3.4' }; + safe = require('safetydance'); describe('Apps', function () { - var ADMIN_0 = common.ADMIN; + const { domainSetup, cleanup, app, admin, user } = common; - var USER_0 = common.USER; - - var USER_1 = { - id: 'uuid2134', - username: 'uuid2134', - password: 'secret', - email: 'safe1@me.com', - fallbackEmail: 'safe1@me.com', - salt: 'morton', - createdAt: 'sometime back', - resetToken: hat(256), - displayName: '', - groupIds: [ 'somegroup' ], - role: 'user', - source: '', - avatar: constants.AVATAR_NONE - }; - - var GROUP_0 = { - id: 'somegroup', - name: 'group0', - source: '' - }; - var GROUP_1 = { - id: 'anothergroup', - name: 'group1', - source: 'ldap' - }; - - const DOMAIN_0 = common.DOMAIN; - - const DOMAIN_1 = { - domain: 'example2.com', - zoneName: 'example2.com', - provider: 'noop', - config: { }, - fallbackCertificate: null, - tlsConfig: { provider: 'fallback' }, - wellKnown: null - }; - - var APP_0 = { - id: 'appid-0', - appStoreId: 'appStoreId-0', - location: 'some-location-0', - domain: DOMAIN_0.domain, - fqdn: 'some-location-0.' + DOMAIN_0.domain, - manifest: { - version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0', - tcpPorts: { - PORT: { - description: 'this is a port that i expose', - containerPort: '1234' - } - } - }, - portBindings: { PORT: 5678 }, - accessRestriction: null, - memoryLimit: 0, - cpuShares: 512, - reverseProxyConfig: null, - sso: false, - mailboxDomain: DOMAIN_0.domain, - env: { - 'CUSTOM_KEY': 'CUSTOM_VALUE' - }, - dataDir: '', - installationState: 'installed', - runState: 'running' - }; - - var APP_1 = { - id: 'appid-1', - appStoreId: 'appStoreId-1', - location: 'some-location-1', - domain: DOMAIN_0.domain, - fqdn: 'some-location-1.' + DOMAIN_0.domain, - manifest: { - version: '0.1', dockerImage: 'docker/app1', healthCheckPath: '/', httpPort: 80, title: 'app1', - tcpPorts: {} - }, - portBindings: {}, - accessRestriction: { users: [ 'someuser' ], groups: [ GROUP_0.id ] }, - memoryLimit: 0, - cpuShares: 512, - env: {}, - dataDir: '', - mailboxDomain: DOMAIN_0.domain, - installationState: 'installed', - runState: 'running' - }; - - var APP_2 = { - id: 'appid-2', - appStoreId: 'appStoreId-2', - location: 'some-location-2', - domain: DOMAIN_1.domain, - fqdn: 'some-location-2.' + DOMAIN_1.domain, - manifest: { - version: '0.1', dockerImage: 'docker/app2', healthCheckPath: '/', httpPort: 80, title: 'app2', - tcpPorts: {} - }, - portBindings: {}, - accessRestriction: { users: [ 'someuser', USER_0.id ], groups: [ GROUP_1.id ] }, - memoryLimit: 0, - cpuShares: 512, - reverseProxyConfig: null, - sso: false, - env: {}, - dataDir: '', - mailboxDomain: DOMAIN_0.domain, - installationState: 'installed', - runState: 'running' - }; - - before(function (done) { - common.setup(function (error) { - if (error) return done(error); - - async.series([ - userdb.add.bind(null, USER_1.id, USER_1), - domains.add.bind(null, DOMAIN_1.domain, DOMAIN_1, AUDIT_SOURCE), - appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0), - appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, apps._translatePortBindings(APP_1.portBindings, APP_1.manifest), APP_1), - appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, apps._translatePortBindings(APP_2.portBindings, APP_2.manifest), APP_2), - ], done); - }); - }); - - after(common.cleanup); + before(domainSetup); + after(cleanup); describe('validatePortBindings', function () { it('does not allow invalid host port', function () { @@ -183,39 +49,6 @@ describe('Apps', function () { }); }); - describe('getters', function () { - it('cannot get invalid app', function (done) { - apps.get('nope', function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.be(BoxError.NOT_FOUND); - done(); - }); - }); - - it('can get valid app', function (done) { - apps.get(APP_0.id, function (error, app) { - expect(error).to.be(null); - expect(app).to.be.ok(); - expect(app.iconUrl).to.be(null); - expect(app.fqdn).to.eql(APP_0.location + '.' + DOMAIN_0.domain); - expect(app.memoryLimit).to.eql(0); - expect(app.cpuShares).to.eql(512); - done(); - }); - }); - - it('can getAll', function (done) { - apps.getAll(function (error, apps) { - expect(error).to.be(null); - expect(apps).to.be.an(Array); - expect(apps[1].id).to.be(APP_0.id); - expect(apps[1].iconUrl).to.be(null); - expect(apps[1].fqdn).to.eql(APP_0.location + '.' + DOMAIN_0.domain); - done(); - }); - }); - }); - describe('validateAccessRestriction', function () { it('allows null input', function () { expect(apps._validateAccessRestriction(null)).to.eql(null); @@ -242,155 +75,188 @@ describe('Apps', function () { const someuser = { id: 'someuser', groupIds: [], role: 'user' }; const adminuser = { id: 'adminuser', groupIds: [ 'groupie' ], role: 'admin' }; - it('returns true for unrestricted access', function (done) { - apps.hasAccessTo({ accessRestriction: null }, someuser, function (error, access) { - expect(error).to.be(null); - expect(access).to.be(true); - done(); - }); + it('returns true for unrestricted access', function () { + expect(apps.hasAccessTo({ accessRestriction: null }, someuser)).to.be(true); }); - it('returns true for allowed user', function (done) { - apps.hasAccessTo({ accessRestriction: { users: [ 'someuser' ] } }, someuser, function (error, access) { - expect(error).to.be(null); - expect(access).to.be(true); - done(); - }); + it('returns true for allowed user', function () { + expect(apps.hasAccessTo({ accessRestriction: { users: [ 'someuser' ] } }, someuser)).to.be(true); }); - it('returns true for allowed user with multiple allowed', function (done) { - apps.hasAccessTo({ accessRestriction: { users: [ 'foo', 'someuser', 'anotheruser' ] } }, someuser, function (error, access) { - expect(error).to.be(null); - expect(access).to.be(true); - done(); - }); + it('returns true for allowed user with multiple allowed', function () { + expect(apps.hasAccessTo({ accessRestriction: { users: [ 'foo', 'someuser', 'anotheruser' ] } }, someuser)).to.be(true); }); - it('returns false for not allowed user', function (done) { - apps.hasAccessTo({ accessRestriction: { users: [ 'foo' ] } }, someuser, function (error, access) { - expect(error).to.be(null); - expect(access).to.be(false); - done(); - }); + it('returns false for not allowed user', function () { + expect(apps.hasAccessTo({ accessRestriction: { users: [ 'foo' ] } }, someuser)).to.be(false); }); - it('returns false for not allowed user with multiple allowed', function (done) { - apps.hasAccessTo({ accessRestriction: { users: [ 'foo', 'anotheruser' ] } }, someuser, function (error, access) { - expect(error).to.be(null); - expect(access).to.be(false); - done(); - }); + it('returns false for not allowed user with multiple allowed', function () { + expect(apps.hasAccessTo({ accessRestriction: { users: [ 'foo', 'anotheruser' ] } }, someuser)).to.be(false); }); - it('returns false for no group or user', function (done) { - apps.hasAccessTo({ accessRestriction: { users: [ ], groups: [ ] } }, someuser, function (error, access) { - expect(error).to.be(null); - expect(access).to.be(false); - done(); - }); + it('returns false for no group or user', function () { + expect(apps.hasAccessTo({ accessRestriction: { users: [ ], groups: [ ] } }, someuser)).to.be(false); }); - it('returns false for invalid group or user', function (done) { - apps.hasAccessTo({ accessRestriction: { users: [ ], groups: [ 'nop' ] } }, someuser, function (error, access) { - expect(error).to.be(null); - expect(access).to.be(false); - done(); - }); + it('returns false for invalid group or user', function () { + expect(apps.hasAccessTo({ accessRestriction: { users: [ ], groups: [ 'nop' ] } }, someuser)).to.be(false); }); - it('returns true for admin user', function (done) { - apps.hasAccessTo({ accessRestriction: { users: [ ], groups: [ 'nop' ] } }, adminuser, function (error, access) { - expect(error).to.be(null); - expect(access).to.be(true); - done(); - }); + it('returns true for admin user', function () { + expect(apps.hasAccessTo({ accessRestriction: { users: [ ], groups: [ 'nop' ] } }, adminuser)).to.be(true); }); }); - describe('getAllByUser', function () { - it('succeeds for USER_0', function (done) { - apps.getAllByUser(USER_0, function (error, result) { - expect(error).to.equal(null); - expect(result.length).to.equal(3); - expect(result[0].id).to.equal(common.APP.id); - expect(result[1].id).to.equal(APP_0.id); - expect(result[2].id).to.equal(APP_2.id); - done(); - }); + describe('crud', function () { + it('cannot get invalid app', async function () { + const result = await apps.get('nope'); + expect(result).to.be(null); }); - it('succeeds for USER_1', function (done) { - apps.getAllByUser(USER_1, function (error, result) { - expect(error).to.equal(null); - expect(result.length).to.equal(3); - expect(result[0].id).to.equal(common.APP.id); - expect(result[1].id).to.equal(APP_0.id); - expect(result[2].id).to.equal(APP_1.id); - done(); - }); + it('can add app', async function () { + await apps.add(app.id, app.appStoreId, app.manifest, app.location, app.domain, app.portBindings, app); }); - it('returns all apps for admin', function (done) { - apps.getAllByUser(ADMIN_0, function (error, result) { - expect(error).to.equal(null); - expect(result.length).to.equal(4); - expect(result[0].id).to.equal(common.APP.id); - expect(result[1].id).to.equal(APP_0.id); - expect(result[2].id).to.equal(APP_1.id); - expect(result[3].id).to.equal(APP_2.id); - done(); - }); + it('cannot add with same app id', async function () { + const [error] = await safe(apps.add(app.id, app.appStoreId, app.manifest, app.location, app.domain, app.portBindings, app)); + expect(error.reason).to.be(BoxError.ALREADY_EXISTS); + }); + + it('cannot add with same app id', async function () { + const [error] = await safe(apps.add(app.id, app.appStoreId, app.manifest, app.location, app.domain, app.portBindings, app)); + expect(error.reason).to.be(BoxError.ALREADY_EXISTS); + }); + + it('can get app', async function () { + const result = await apps.get(app.id); + expect(result.manifest).to.eql(app.manifest); + expect(result.portBindings).to.eql({}); + expect(result.location).to.eql(app.location); + }); + + it('can list apps', async function () { + const result = await apps.list(); + expect(result.length).to.be(1); + expect(result[0].manifest).to.eql(app.manifest); + expect(result[0].portBindings).to.eql({}); + expect(result[0].location).to.eql(app.location); + }); + + it('can listByUser', async function () { + let result = await apps.listByUser(admin); + expect(result.length).to.be(1); + + result = await apps.listByUser(user); + expect(result.length).to.be(1); + }); + + it('update succeeds', async function () { + const data = { + installationState: 'some-other-status', + location:'some-other-location', + domain: app.domain, // needs to be set whenever location is set + manifest: Object.assign({}, app.manifest, { version: '0.2.0' }), + accessRestriction: '', + memoryLimit: 1337, + cpuShares: 102, + }; + + await apps.update(app.id, data); + const newApp = await apps.get(app.id); + expect(newApp.installationState).to.be('some-other-status'); + expect(newApp.location).to.be('some-other-location'); + expect(newApp.manifest.version).to.be('0.2.0'); + expect(newApp.accessRestriction).to.be(''); + expect(newApp.memoryLimit).to.be(1337); + expect(newApp.cpuShares).to.be(102); + }); + + it('update of nonexisting app fails', async function () { + const [error] = await safe(apps.update('random', { installationState: app.installationState, location: app.location })); + expect(error.reason).to.be(BoxError.NOT_FOUND); + }); + + it('delete succeeds', async function () { + await apps.del(app.id); + }); + + it('cannot delete previously delete record', async function () { + const [error] = await safe(apps.del(app.id)); + expect(error.reason).to.be(BoxError.NOT_FOUND); + }); + }); + + describe('setHealth', function () { + before(async function () { + await apps.add(app.id, app.appStoreId, app.manifest, app.location, app.domain, app.portBindings, app); + }); + + it('can set app as healthy', async function () { + const result = await apps.get(app.id); + expect(result.health).to.be(null); + + await apps.setHealth(app.id, apps.HEALTH_HEALTHY, new Date()); + }); + + it('did set app as healthy', async function () { + const result = await apps.get(app.id); + expect(result.health).to.be(apps.HEALTH_HEALTHY); + }); + + it('cannot set health of unknown app', async function () { + const [error] = await safe(apps.setHealth('randomId', apps.HEALTH_HEALTHY, new Date())); + expect(error.reason).to.be(BoxError.NOT_FOUND); }); }); describe('configureInstalledApps', function () { - before(function (done) { - async.series([ - appdb.update.bind(null, APP_0.id, { installationState: apps.ISTATE_INSTALLED }), - appdb.update.bind(null, APP_1.id, { installationState: apps.ISTATE_ERROR }), - appdb.update.bind(null, APP_2.id, { installationState: apps.ISTATE_INSTALLED }) - ], done); + const app1 = Object.assign({}, app, { id: 'id1', installationState: apps.ISTATE_ERROR, location: 'loc1' }); + const app2 = Object.assign({}, app, { id: 'id2', installationState: apps.ISTATE_INSTALLED, location: 'loc2' }); + + before(async function () { + await apps.update(app.id, { installationState: apps.ISTATE_INSTALLED }); + await apps.add(app1.id, app1.appStoreId, app1.manifest, app1.location, app1.domain, app1.portBindings, app1); + await apps.add(app2.id, app2.appStoreId, app2.manifest, app2.location, app2.domain, app2.portBindings, app2); }); - it('can mark apps for reconfigure', function (done) { - apps.configureInstalledApps(function (error) { - expect(error).to.be(null); + after(async function () { + await apps.del(app1.id); + await apps.del(app2.id); + }); - apps.getAll(function (error, result) { - expect(result[0].installationState).to.be(apps.ISTATE_PENDING_CONFIGURE); - expect(result[1].installationState).to.be(apps.ISTATE_PENDING_CONFIGURE); - expect(result[2].installationState).to.be(apps.ISTATE_ERROR); - expect(result[3].installationState).to.be(apps.ISTATE_PENDING_CONFIGURE); + it('can mark apps for reconfigure', async function () { + await apps.configureInstalledApps(); - done(); - }); - }); + const result = await apps.list(); + expect(result[0].installationState).to.be(apps.ISTATE_PENDING_CONFIGURE); + expect(result[1].installationState).to.be(apps.ISTATE_ERROR); + expect(result[2].installationState).to.be(apps.ISTATE_PENDING_CONFIGURE); }); }); describe('restoreInstalledApps', function () { - before(function (done) { - async.series([ - appdb.update.bind(null, APP_0.id, { installationState: apps.ISTATE_INSTALLED }), - appdb.update.bind(null, APP_1.id, { installationState: apps.ISTATE_ERROR }), - appdb.update.bind(null, APP_2.id, { installationState: apps.ISTATE_INSTALLED }) - ], done); + const app1 = Object.assign({}, app, { id: 'id1', installationState: apps.ISTATE_ERROR, location: 'loc1' }); + const app2 = Object.assign({}, app, { id: 'id2', installationState: apps.ISTATE_INSTALLED, location: 'loc2' }); + + before(async function () { + await apps.update(app.id, { installationState: apps.ISTATE_INSTALLED }); + await apps.add(app1.id, app1.appStoreId, app1.manifest, app1.location, app1.domain, app1.portBindings, app1); + await apps.add(app2.id, app2.appStoreId, app2.manifest, app2.location, app2.domain, app2.portBindings, app2); }); - it('can mark apps for restore', function (done) { - apps.restoreInstalledApps({}, function (error) { - expect(error).to.be(null); + after(async function () { + await apps.del(app1.id); + await apps.del(app2.id); + }); - apps.getAll(function (error, result) { - expect(result[0].installationState).to.be(apps.ISTATE_PENDING_INSTALL); - expect(result[1].installationState).to.be(apps.ISTATE_PENDING_INSTALL); - expect(result[2].installationState).to.be(apps.ISTATE_ERROR); - expect(result[3].installationState).to.be(apps.ISTATE_PENDING_INSTALL); + it('can mark apps for reconfigure', async function () { + await apps.restoreInstalledApps({}); - done(); - }); - }); + const result = await apps.list(); + expect(result[0].installationState).to.be(apps.ISTATE_PENDING_INSTALL); + expect(result[1].installationState).to.be(apps.ISTATE_ERROR); + expect(result[2].installationState).to.be(apps.ISTATE_PENDING_INSTALL); }); }); }); - diff --git a/src/test/appstore-test.js b/src/test/appstore-test.js index 2c128e75f..fa6e11b63 100644 --- a/src/test/appstore-test.js +++ b/src/test/appstore-test.js @@ -23,20 +23,16 @@ describe('Appstore', function () { beforeEach(nock.cleanAll); - it('can purchase an app', function (done) { + it('can purchase an app', async function () { const scope1 = nock(mockApiServerOrigin) .post(`/api/v1/cloudronapps?accessToken=${appstoreToken}`, function () { return true; }) .reply(201, {}); - appstore.purchaseApp({ appId: APP_ID, appstoreId: APPSTORE_APP_ID, manifestId: APPSTORE_APP_ID }, function (error) { - expect(error).to.not.be.ok(); - expect(scope1.isDone()).to.be.ok(); - - done(); - }); + await appstore.purchaseApp({ appId: APP_ID, appstoreId: APPSTORE_APP_ID, manifestId: APPSTORE_APP_ID }); + expect(scope1.isDone()).to.be.ok(); }); - it('unpurchase succeeds if app was never purchased', function (done) { + it('unpurchase succeeds if app was never purchased', async function () { const scope1 = nock(mockApiServerOrigin) .get(`/api/v1/cloudronapps/${APP_ID}?accessToken=${appstoreToken}`) .reply(404, {}); @@ -45,16 +41,12 @@ describe('Appstore', function () { .delete(`/api/v1/cloudronapps/${APP_ID}?accessToken=${appstoreToken}`, function () { return true; }) .reply(204, {}); - appstore.unpurchaseApp(APP_ID, { appstoreId: APPSTORE_APP_ID, manifestId: APPSTORE_APP_ID }, function (error) { - expect(error).to.not.be.ok(); - expect(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.not.be.ok(); - - done(); - }); + await appstore.unpurchaseApp(APP_ID, { appstoreId: APPSTORE_APP_ID, manifestId: APPSTORE_APP_ID }); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.not.be.ok(); }); - it('can unpurchase an app', function (done) { + it('can unpurchase an app', async function () { const scope1 = nock(mockApiServerOrigin) .get(`/api/v1/cloudronapps/${APP_ID}?accessToken=${appstoreToken}`) .reply(200, {}); @@ -63,12 +55,8 @@ describe('Appstore', function () { .delete(`/api/v1/cloudronapps/${APP_ID}?accessToken=${appstoreToken}`, function () { return true; }) .reply(204, {}); - appstore.unpurchaseApp(APP_ID, { appstoreId: APPSTORE_APP_ID, manifestId: APPSTORE_APP_ID }, function (error) { - expect(error).to.not.be.ok(); - expect(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); - - done(); - }); + await appstore.unpurchaseApp(APP_ID, { appstoreId: APPSTORE_APP_ID, manifestId: APPSTORE_APP_ID }); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); }); }); diff --git a/src/test/backuptask-test.js b/src/test/backuptask-test.js index 25b6c79db..51a633229 100644 --- a/src/test/backuptask-test.js +++ b/src/test/backuptask-test.js @@ -76,10 +76,10 @@ describe('backuptask', function () { schedulePattern: '00 00 23 * * *' }; - before(function (done) { + before(async function () { fs.rmSync(backupConfig.backupFolder, { recursive: true, force: true }); - settings.setBackupConfig(backupConfig, done); + await settings.setBackupConfig(backupConfig); }); after(function () { diff --git a/src/test/common.js b/src/test/common.js index 3ca535efe..69b19ae19 100644 --- a/src/test/common.js +++ b/src/test/common.js @@ -1,7 +1,6 @@ 'use strict'; -const appdb = require('../appdb.js'), - apps = require('../apps.js'), +const apps = require('../apps.js'), async = require('async'), blobs = require('../blobs.js'), constants = require('../constants.js'), @@ -55,6 +54,7 @@ const domain = { tlsConfig: { provider: 'fallback' }, wellKnown: null }; +Object.freeze(domain); const auditSource = { ip: '1.2.3.4' }; @@ -109,6 +109,7 @@ const app = { alternateDomains: [], aliasDomains: [] }; +Object.freeze(app); exports = module.exports = { createTree, @@ -154,26 +155,22 @@ function createTree(root, obj) { createSubTree(obj, root); } -function databaseSetup(done) { +async function databaseSetup() { nock.cleanAll(); - async.series([ - database.initialize, - database._clear, - settings._setApiServerOrigin.bind(null, exports.mockApiServerOrigin), - settings.setDashboardLocation.bind(null, exports.dashboardDomain, exports.dashboardFqdn), - settings.initCache, - blobs.initSecrets, - ], done); + await database.initialize(); + await database._clear(); + await settings._setApiServerOrigin(exports.mockApiServerOrigin); + await settings.setDashboardLocation(exports.dashboardDomain, exports.dashboardFqdn); + await settings.initCache(); + await blobs.initSecrets(); } -function domainSetup(done) { +async function domainSetup() { nock.cleanAll(); - async.series([ - databaseSetup, - domains.add.bind(null, domain.domain, domain, auditSource), - ], done); + await databaseSetup(); + await domains.add(domain.domain, domain, auditSource); } function setup(done) { @@ -183,7 +180,7 @@ function setup(done) { const result = await users.createOwner(admin.email, admin.username, admin.password, admin.displayName, auditSource); admin.id = result; }, - appdb.add.bind(null, app.id, app.appStoreId, app.manifest, app.location, app.domain, app.portBindings, app), + apps.add.bind(null, app.id, app.appStoreId, app.manifest, app.location, app.domain, app.portBindings, app), settings._set.bind(null, settings.CLOUDRON_TOKEN_KEY, exports.appstoreToken), // appstore token async function createUser() { const result = await users.add(user.email, user, auditSource); @@ -194,14 +191,12 @@ function setup(done) { ], done); } -function cleanup(done) { +async function cleanup() { nock.cleanAll(); mailer._mailQueue = []; - async.series([ - database._clear, - database.uninitialize - ], done); + await database._clear(); + await database.uninitialize(); } function clearMailQueue() { diff --git a/src/test/database-test.js b/src/test/database-test.js index 34357671c..5dd0de659 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -1,295 +1,31 @@ /* global it:false */ /* global describe:false */ /* global before:false */ -/* global after:false */ 'use strict'; -const appdb = require('../appdb.js'), - apps = require('../apps.js'), - async = require('async'), - BoxError = require('../boxerror.js'), - database = require('../database'), - domains = require('../domains.js'), - expect = require('expect.js'), - reverseProxy = require('../reverseproxy.js'), - _ = require('underscore'); - -const DOMAIN_0 = { - domain: 'foobar.com', - zoneName: 'foobar.com', - provider: 'digitalocean', - config: { token: 'abcd' }, - tlsConfig: { provider: 'fallback' }, - wellKnown: null -}; -DOMAIN_0.fallbackCertificate = reverseProxy.generateFallbackCertificateSync(DOMAIN_0.domain); - -const auditSource = { ip: '1.2.3.4' }; - -const DOMAIN_1 = { - domain: 'foo.cloudron.io', - zoneName: 'cloudron.io', - provider: 'manual', - config: null, - tlsConfig: { provider: 'fallback' }, - wellKnown: null -}; -DOMAIN_1.fallbackCertificate = reverseProxy.generateFallbackCertificateSync(DOMAIN_1.domain); +const database = require('../database'), + expect = require('expect.js'); describe('database', function () { - before(function (done) { - async.series([ - database.initialize, - database._clear - ], done); - }); - - after(function (done) { - async.series([ - database._clear, - database.uninitialize - ], done); - }); - - describe('apps', function () { - var APP_0 = { - id: 'appid-0', - appStoreId: 'appStoreId-0', - installationState: apps.ISTATE_PENDING_INSTALL, - error: null, - runState: 'running', - location: 'some-location-0', - domain: DOMAIN_0.domain, - manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' }, - containerId: null, - containerIp: null, - portBindings: { port: { hostPort: 5678, type: 'tcp' } }, - health: null, - accessRestriction: null, - memoryLimit: 4294967296, - cpuShares: 256, - sso: true, - debugMode: null, - reverseProxyConfig: {}, - enableBackup: true, - enableMailbox: true, - alternateDomains: [], - aliasDomains: [], - env: { - 'CUSTOM_KEY': 'CUSTOM_VALUE' - }, - mailboxName: 'talktome', - mailboxDomain: DOMAIN_0.domain, - enableAutomaticUpdate: true, - dataDir: null, - tags: [], - label: null, - taskId: null, - mounts: [], - proxyAuth: false, - servicesConfig: {}, - hasIcon: false, - hasAppStoreIcon: false - }; - - var APP_1 = { - id: 'appid-1', - appStoreId: 'appStoreId-1', - installationState: apps.ISTATE_PENDING_INSTALL, // app health tests rely on this initial state - error: null, - runState: 'running', - location: 'some-location-1', - domain: DOMAIN_0.domain, - manifest: { version: '0.2', dockerImage: 'docker/app1', healthCheckPath: '/', httpPort: 80, title: 'app1' }, - containerId: null, - containerIp: null, - portBindings: { }, - health: null, - accessRestriction: { users: [ 'foobar' ] }, - memoryLimit: 0, - cpuShares: 512, - sso: true, - debugMode: null, - reverseProxyConfig: {}, - enableBackup: true, - alternateDomains: [], - aliasDomains: [], - env: {}, - enableMailbox: true, - mailboxName: 'callme', - mailboxDomain: DOMAIN_0.domain, - enableAutomaticUpdate: true, - dataDir: null, - tags: [], - label: null, - taskId: null, - mounts: [], - proxyAuth: false, - servicesConfig: {}, - hasIcon: false, - hasAppStoreIcon: false - }; - - before(function (done) { - async.series([ - database._clear, - domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, auditSource) - ], done); + describe('init', function () { + it('can init database', async function () { + await database.initialize(); }); - after(function (done) { - database._clear(done); + it('can clear database', async function () { + await database._clear(); }); - it('add fails due to missing arguments', function () { - expect(function () { appdb.add(APP_0.id, APP_0.manifest, APP_0.installationState, function () {}); }).to.throwError(); - expect(function () { appdb.add(APP_0.id, function () {}); }).to.throwError(); - }); - - it('add succeeds', function (done) { - appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0, function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('getPortBindings succeeds', function (done) { - appdb.getPortBindings(APP_0.id, function (error, bindings) { - expect(error).to.be(null); - expect(bindings).to.be.an(Object); - expect(bindings).to.be.eql({ port: { hostPort: '5678', type: 'tcp' } }); - done(); - }); - }); - - it('add of same app fails', function (done) { - appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, [], APP_0, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.ALREADY_EXISTS); - done(); - }); - }); - - it('get succeeds', function (done) { - appdb.get(APP_0.id, function (error, result) { - expect(error).to.be(null); - expect(result).to.be.an('object'); - expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime', 'resetTokenCreationTime'])).to.be.eql(APP_0); - done(); - }); - }); - - it('get of nonexisting code fails', function (done) { - appdb.get(APP_1.id, function (error, result) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.NOT_FOUND); - expect(result).to.not.be.ok(); - done(); - }); - }); - - it('update succeeds', function (done) { - APP_0.installationState = 'some-other-status'; - APP_0.location = 'some-other-location'; - APP_0.manifest.version = '0.2'; - APP_0.accessRestriction = ''; - APP_0.memoryLimit = 1337; - APP_0.cpuShares = 1024; - - var data = { - installationState: APP_0.installationState, - location: APP_0.location, - domain: APP_0.domain, - manifest: APP_0.manifest, - accessRestriction: APP_0.accessRestriction, - memoryLimit: APP_0.memoryLimit, - cpuShares: APP_0.cpuShares - }; - - appdb.update(APP_0.id, data, function (error) { - expect(error).to.be(null); - - appdb.get(APP_0.id, function (error, result) { - expect(error).to.be(null); - expect(result).to.be.an('object'); - expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime','resetTokenCreationTime'])).to.be.eql(APP_0); - done(); - }); - }); - }); - - it('update of nonexisting app fails', function (done) { - appdb.update(APP_1.id, { installationState: APP_1.installationState, location: APP_1.location }, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.NOT_FOUND); - done(); - }); - }); - - it('add second app succeeds', function (done) { - appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, [], APP_1, function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('getAll succeeds', function (done) { - appdb.getAll(function (error, result) { - expect(error).to.be(null); - expect(result).to.be.an(Array); - expect(result.length).to.be(2); - expect(_.omit(result[0], ['creationTime', 'updateTime','ts', 'healthTime', 'resetTokenCreationTime'])).to.be.eql(APP_0); - expect(_.omit(result[1], ['creationTime', 'updateTime','ts', 'healthTime', 'resetTokenCreationTime'])).to.be.eql(APP_1); - done(); - }); - }); - - it('delete succeeds', function (done) { - appdb.del(APP_0.id, function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('getPortBindings should be empty', function (done) { - appdb.getPortBindings(APP_0.id, function (error, bindings) { - expect(error).to.be(null); - expect(bindings).to.be.an(Object); - expect(bindings).to.be.eql({ }); - done(); - }); - }); - - it('cannot delete previously delete record', function (done) { - appdb.del(APP_0.id, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.NOT_FOUND); - done(); - }); - }); - - it('can set app as healthy', function (done) { - appdb.setHealth(APP_1.id, apps.HEALTH_HEALTHY, new Date(), function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('cannot set health of unknown app', function (done) { - appdb.setHealth('randomId', apps.HEALTH_HEALTHY, new Date(), function (error) { - expect(error).to.be.ok(); - done(); - }); + it('can uninitialize database', async function () { + await database.uninitialize(); }); }); describe('importFromFile', function () { - before(function (done) { - async.series([ - database.initialize, - database._clear - ], done); + before(async function () { + await database.initialize(); + await database._clear(); }); it('cannot import from non-existent file', function (done) { diff --git a/src/test/dns-test.js b/src/test/dns-test.js index abae0c4db..73857704e 100644 --- a/src/test/dns-test.js +++ b/src/test/dns-test.js @@ -21,10 +21,10 @@ describe('DNS', function () { }); it('cannot have >63 length subdomains', function () { - var s = Array(64).fill('s').join(''); + const s = Array(64).fill('s').join(''); expect(dns.validateHostname(s, domain)).to.be.an(Error); - domain.zoneName = `dev.${s}.example.com`; - expect(dns.validateHostname(`dev.${s}`, domain)).to.be.an(Error); + const domainCopy = Object.assign({}, domain, { zoneName: `dev.${s}.example.com` }); + expect(dns.validateHostname(`dev.${s}`, domainCopy)).to.be.an(Error); }); it('allows only alphanumerics and hypen', function () { @@ -39,7 +39,7 @@ describe('DNS', function () { }); it('total length cannot exceed 255', function () { - var s = ''; + let s = ''; for (var i = 0; i < (255 - 'example.com'.length); i++) s += 's'; expect(dns.validateHostname(s, domain)).to.be.an(Error); diff --git a/src/test/domains-test.js b/src/test/domains-test.js index 3e75af166..cfab4fd93 100644 --- a/src/test/domains-test.js +++ b/src/test/domains-test.js @@ -6,13 +6,12 @@ 'use strict'; -const appdb = require('../appdb.js'), +const apps = require('../apps.js'), BoxError = require('../boxerror.js'), common = require('./common.js'), domains = require('../domains.js'), expect = require('expect.js'), - safe = require('safetydance'), - util = require('util'); + safe = require('safetydance'); describe('Domains', function () { const { setup, cleanup, domain, app, auditSource } = common; @@ -105,13 +104,13 @@ describe('Domains', function () { it('cannot delete referenced domain', async function () { const appCopy = Object.assign({}, app, { id: 'into', location: 'xx', domain: DOMAIN_0.domain, portBindings: {} }); - await util.promisify(appdb.add)(appCopy.id, appCopy.appStoreId, appCopy.manifest, appCopy.location, appCopy.domain, appCopy.portBindings, appCopy); + await apps.add(appCopy.id, appCopy.appStoreId, appCopy.manifest, appCopy.location, appCopy.domain, appCopy.portBindings, appCopy); const [error] = await safe(domains.del(DOMAIN_0.domain, auditSource)); expect(error.reason).to.equal(BoxError.CONFLICT); expect(error.message).to.contain('Domain is in use by one or more app'); - await util.promisify(appdb.del)(appCopy.id); + await apps.del(appCopy.id); }); it('can delete existing domain', async function () { diff --git a/src/test/ldap-test.js b/src/test/ldap-test.js index 1cbd10083..661edc4d0 100644 --- a/src/test/ldap-test.js +++ b/src/test/ldap-test.js @@ -61,6 +61,7 @@ async function ldapSearch(dn, opts) { describe('Ldap', function () { const { setup, cleanup, admin, user, app, domain, auditSource } = common; let group; + const mockApp = Object.assign({}, app); const mailboxName = 'support'; const mailbox = `support@${domain.domain}`; @@ -79,7 +80,7 @@ describe('Ldap', function () { } ], done); - ldapServer._MOCK_APP = app; + ldapServer._MOCK_APP = mockApp; }); after(function (done) { @@ -124,18 +125,18 @@ describe('Ldap', function () { describe('non-admin bind', function () { it('succeeds with null accessRestriction', async function () { - app.accessRestriction = null; + mockApp.accessRestriction = null; await ldapBind(`cn=${user.id},ou=users,dc=cloudron`, user.password); }); it('fails without accessRestriction', async function () { - app.accessRestriction = { users: [], groups: [] }; + mockApp.accessRestriction = { users: [], groups: [] }; const [error] = await safe(ldapBind(`cn=${user.id},ou=users,dc=cloudron`, user.password)); expect(error).to.be.a(ldap.NoSuchObjectError); }); it('succeeds with accessRestriction', async function () { - app.accessRestriction = { users: [ user.id ], groups: [] }; + mockApp.accessRestriction = { users: [ user.id ], groups: [] }; await ldapBind(`cn=${user.id},ou=users,dc=cloudron`, user.password); }); }); @@ -182,7 +183,7 @@ describe('Ldap', function () { }); it('can always lists admins', async function () { - app.accessRestriction = { users: [], groups: [] }; + mockApp.accessRestriction = { users: [], groups: [] }; const entries = await ldapSearch('ou=users,dc=cloudron', { filter: 'objectcategory=person' }); expect(entries.length).to.equal(1); expect(entries[0].username).to.equal(admin.username.toLowerCase()); @@ -190,7 +191,7 @@ describe('Ldap', function () { }); it ('does only list users who have access', async function () { - app.accessRestriction = { users: [], groups: [ group.id ] }; + mockApp.accessRestriction = { users: [], groups: [ group.id ] }; const entries = await ldapSearch('ou=users,dc=cloudron', { filter: 'objectcategory=person' }); expect(entries.length).to.equal(2); entries.sort(function (a, b) { return a.username > b.username; }); @@ -237,7 +238,7 @@ describe('Ldap', function () { }); it ('does only list users who have access', async function () { - app.accessRestriction = { users: [], groups: [ group.id ] }; + mockApp.accessRestriction = { users: [], groups: [ group.id ] }; const entries = await ldapSearch('ou=groups,dc=cloudron', { filter: '&(objectclass=group)(cn=*)' }); expect(entries.length).to.equal(2); expect(entries[0].cn).to.equal('users'); @@ -250,7 +251,7 @@ describe('Ldap', function () { }); it ('succeeds with pagination', async function () { - app.accessRestriction = null; + mockApp.accessRestriction = null; const entries = await ldapSearch('ou=groups,dc=cloudron', { filter: 'objectclass=group', paged: true }); expect(entries.length).to.equal(2); diff --git a/src/test/server-test.js b/src/test/server-test.js index d55f3331d..e27234950 100644 --- a/src/test/server-test.js +++ b/src/test/server-test.js @@ -6,24 +6,18 @@ 'use strict'; -var constants = require('../constants.js'), +const constants = require('../constants.js'), database = require('../database.js'), expect = require('expect.js'), + safe = require('safetydance'), + server = require('../server.js'), superagent = require('superagent'), - server = require('../server.js'); + util = require('util'); -var SERVER_URL = 'http://localhost:' + constants.PORT; +const SERVER_URL = 'http://localhost:' + constants.PORT; describe('Server', function () { describe('startup', function () { - it('start fails due to wrong arguments', function (done) { - expect(function () { server.start(); }).to.throwException(); - expect(function () { server.start('foobar', function () {}); }).to.throwException(); - expect(function () { server.start(1337, function () {}); }).to.throwException(); - - done(); - }); - it('succeeds', function (done) { server.start(function (error) { expect(error).to.not.be.ok(); @@ -31,11 +25,9 @@ describe('Server', function () { }); }); - it('is reachable', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) { - expect(res.statusCode).to.equal(200); - done(err); - }); + it('is reachable', async function () { + const response = await superagent.get(SERVER_URL + '/api/v1/cloudron/status'); + expect(response.status).to.equal(200); }); it('should fail because already running', function (done) { @@ -56,43 +48,28 @@ describe('Server', function () { server.start(done); }); - after(function (done) { - database._clear(function (error) { - expect(!error).to.be.ok(); - server.stop(function () { - done(); - }); - }); + after(async function () { + await database._clear(); + await util.promisify(server.stop)(); }); - it('random bad superagents', function (done) { - superagent.get(SERVER_URL + '/random', function (err, res) { - expect(err).to.be.ok(); - expect(res.statusCode).to.equal(404); - done(); - }); + it('random bad superagents', async function () { + const response = await superagent.get(SERVER_URL + '/random').ok(() => true); + expect(response.status).to.equal(404); }); - it('version', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) { - expect(err).to.not.be.ok(); - expect(res.statusCode).to.equal(200); - expect(res.body.version).to.contain('-test'); - done(); - }); + it('version', async function () { + const response = await superagent.get(SERVER_URL + '/api/v1/cloudron/status'); + expect(response.status).to.equal(200); + expect(response.body.version).to.contain('-test'); }); - it('status route is GET', function (done) { - superagent.post(SERVER_URL + '/api/v1/cloudron/status') - .end(function (err, res) { - expect(res.statusCode).to.equal(404); + it('status route is GET', async function () { + const response = await superagent.post(SERVER_URL + '/api/v1/cloudron/status').ok(() => true); + expect(response.status).to.equal(404); - superagent.get(SERVER_URL + '/api/v1/cloudron/status') - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - done(); - }); - }); + const response2 = await superagent.get(SERVER_URL + '/api/v1/cloudron/status'); + expect(response2.statusCode).to.equal(200); }); }); @@ -107,18 +84,14 @@ describe('Server', function () { }); }); - it('config fails due missing token', function (done) { - superagent.get(SERVER_URL + '/api/v1/config', function (err, res) { - expect(res.statusCode).to.equal(401); - done(); - }); + it('config fails due missing token', async function () { + const response = await superagent.get(SERVER_URL + '/api/v1/config').ok(() => true); + expect(response.statusCode).to.equal(401); }); - it('config fails due wrong token', function (done) { - superagent.get(SERVER_URL + '/api/v1/config').query({ access_token: 'somewrongtoken' }).end(function (err, res) { - expect(res.statusCode).to.equal(401); - done(); - }); + it('config fails due wrong token', async function () { + const response = await superagent.get(SERVER_URL + '/api/v1/config').query({ access_token: 'somewrongtoken' }).ok(() => true); + expect(response.status).to.equal(401); }); }); @@ -127,28 +100,15 @@ describe('Server', function () { server.start(done); }); - it('fails due to wrong arguments', function (done) { - expect(function () { server.stop(); }).to.throwException(); - expect(function () { server.stop('foobar'); }).to.throwException(); - expect(function () { server.stop(1337); }).to.throwException(); - expect(function () { server.stop({}); }).to.throwException(); - expect(function () { server.stop({ httpServer: {} }); }).to.throwException(); - - done(); - }); - it('succeeds', function (done) { server.stop(function () { done(); }); }); - it('is not reachable anymore', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (error) { - expect(error).to.not.be(null); - expect(!error.response).to.be.ok(); - done(); - }); + it('is not reachable anymore', async function () { + const [error] = await safe(superagent.get(SERVER_URL + '/api/v1/cloudron/status').ok(() => true)); + expect(error).to.not.be(null); }); }); @@ -159,27 +119,23 @@ describe('Server', function () { }); }); - it('responds to OPTIONS', function (done) { - superagent('OPTIONS', SERVER_URL + '/api/v1/cloudron/status') + it('responds to OPTIONS', async function () { + const response = await superagent('OPTIONS', SERVER_URL + '/api/v1/cloudron/status') .set('Access-Control-Request-Method', 'GET') .set('Access-Control-Request-Headers', 'accept, origin, x-superagented-with') - .set('Origin', 'http://localhost') - .end(function (error, res) { - expect(res.headers['access-control-allow-methods']).to.be('GET, PUT, DELETE, POST, OPTIONS'); - expect(res.headers['access-control-allow-credentials']).to.be('false'); - expect(res.headers['access-control-allow-headers']).to.be('accept, origin, x-superagented-with'); // mirrored from superagent - expect(res.headers['access-control-allow-origin']).to.be('http://localhost'); // mirrors from superagent - done(); - }); + .set('Origin', 'http://localhost'); + + expect(response.headers['access-control-allow-methods']).to.be('GET, PUT, DELETE, POST, OPTIONS'); + expect(response.headers['access-control-allow-credentials']).to.be('false'); + expect(response.headers['access-control-allow-headers']).to.be('accept, origin, x-superagented-with'); // mirrored from superagent + expect(response.headers['access-control-allow-origin']).to.be('http://localhost'); // mirrors from superagent }); - it('does not crash for malformed origin', function (done) { - superagent('OPTIONS', SERVER_URL + '/api/v1/cloudron/status') + it('does not crash for malformed origin', async function () { + const response = await superagent('OPTIONS', SERVER_URL + '/api/v1/cloudron/status') .set('Origin', 'foobar') - .end(function (error, res) { - expect(res.statusCode).to.be(405); - done(); - }); + .ok(() => true); + expect(response.status).to.be(405); }); after(function (done) { diff --git a/src/updatechecker.js b/src/updatechecker.js index dc825edcf..64afc55b0 100644 --- a/src/updatechecker.js +++ b/src/updatechecker.js @@ -14,8 +14,7 @@ const apps = require('./apps.js'), debug = require('debug')('box:updatechecker'), notifications = require('./notifications.js'), paths = require('./paths.js'), - safe = require('safetydance'), - util = require('util'); + safe = require('safetydance'); function setUpdateInfo(state) { // appid -> update info { creationDate, manifest } @@ -39,9 +38,7 @@ async function checkAppUpdates(options) { let state = getUpdateInfo(); let newState = { }; // create new state so that old app ids are removed - const appsGetAllAsync = util.promisify(apps.getAll); - - const result = await appsGetAllAsync(); + const result = await apps.list(); for (const app of result) { if (app.appStoreId === '') continue; // appStoreId can be '' for dev apps diff --git a/src/updater.js b/src/updater.js index 0012baa29..89c84a7e0 100644 --- a/src/updater.js +++ b/src/updater.js @@ -188,58 +188,50 @@ function update(boxUpdateInfo, options, progressCallback, callback) { }); } -function canUpdate(boxUpdateInfo, callback) { +async function canUpdate(boxUpdateInfo) { assert.strictEqual(typeof boxUpdateInfo, 'object'); - assert.strictEqual(typeof callback, 'function'); - apps.getAll(function (error, result) { - if (error) return callback(error); + const result = await apps.list(); - for (let app of result) { - const maxBoxVersion = app.manifest.maxBoxVersion; - if (semver.valid(maxBoxVersion) && semver.gt(boxUpdateInfo.version, maxBoxVersion)) { - return callback(new BoxError(BoxError.BAD_STATE, `Cannot update to v${boxUpdateInfo.version} because ${app.fqdn} has a maxBoxVersion of ${maxBoxVersion}`)); - } + for (let app of result) { + const maxBoxVersion = app.manifest.maxBoxVersion; + if (semver.valid(maxBoxVersion) && semver.gt(boxUpdateInfo.version, maxBoxVersion)) { + throw new BoxError(BoxError.BAD_STATE, `Cannot update to v${boxUpdateInfo.version} because ${app.fqdn} has a maxBoxVersion of ${maxBoxVersion}`); } - - callback(); - }); + } } -function updateToLatest(options, auditSource, callback) { +async function updateToLatest(options, auditSource) { assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - var boxUpdateInfo = updateChecker.getUpdateInfo().box; - if (!boxUpdateInfo) return callback(new BoxError(BoxError.NOT_FOUND, 'No update available')); - if (!boxUpdateInfo.sourceTarballUrl) return callback(new BoxError(BoxError.BAD_STATE, 'No automatic update available')); + const boxUpdateInfo = updateChecker.getUpdateInfo().box; + if (!boxUpdateInfo) throw new BoxError(BoxError.NOT_FOUND, 'No update available'); + if (!boxUpdateInfo.sourceTarballUrl) throw new BoxError(BoxError.BAD_STATE, 'No automatic update available'); - canUpdate(boxUpdateInfo, async function (error) { - if (error) return callback(error); + await canUpdate(boxUpdateInfo); - error = locker.lock(locker.OP_BOX_UPDATE); - if (error) return callback(new BoxError(BoxError.BAD_STATE, `Cannot update now: ${error.message}`)); + const error = locker.lock(locker.OP_BOX_UPDATE); + if (error) throw new BoxError(BoxError.BAD_STATE, `Cannot update now: ${error.message}`); - const [getError, backupConfig] = await safe(settings.getBackupConfig()); - if (getError) return callback(getError); + const [getError, backupConfig] = await safe(settings.getBackupConfig()); + if (getError) throw getError; - const memoryLimit = 'memoryLimit' in backupConfig ? Math.max(backupConfig.memoryLimit/1024/1024, 400) : 400; + const memoryLimit = 'memoryLimit' in backupConfig ? Math.max(backupConfig.memoryLimit/1024/1024, 400) : 400; - const [taskError, taskId] = await safe(tasks.add(tasks.TASK_UPDATE, [ boxUpdateInfo, options ])); - if (taskError) return callback(taskError); + const [taskError, taskId] = await safe(tasks.add(tasks.TASK_UPDATE, [ boxUpdateInfo, options ])); + if (taskError) throw taskError; - eventlog.add(eventlog.ACTION_UPDATE, auditSource, { taskId, boxUpdateInfo }); + eventlog.add(eventlog.ACTION_UPDATE, auditSource, { taskId, boxUpdateInfo }); - tasks.startTask(taskId, { timeout: 20 * 60 * 60 * 1000 /* 20 hours */, nice: 15, memoryLimit }, (error) => { - locker.unlock(locker.OP_BOX_UPDATE); + tasks.startTask(taskId, { timeout: 20 * 60 * 60 * 1000 /* 20 hours */, nice: 15, memoryLimit }, (error) => { + locker.unlock(locker.OP_BOX_UPDATE); - debug('Update failed with error', error); + debug('Update failed with error', error); - const timedOut = error.code === tasks.ETIMEOUT; - eventlog.add(eventlog.ACTION_UPDATE_FINISH, auditSource, { taskId, errorMessage: error.message, timedOut }); - }); - - callback(null, taskId); + const timedOut = error.code === tasks.ETIMEOUT; + eventlog.add(eventlog.ACTION_UPDATE_FINISH, auditSource, { taskId, errorMessage: error.message, timedOut }); }); + + return taskId; }