diff --git a/package-lock.json b/package-lock.json index a6b7e74e9..52b06b2e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -957,9 +957,9 @@ "integrity": "sha1-F03MUSQ7nqwj+NmCFa62aU4uihI=" }, "connect-lastmile": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-1.1.1.tgz", - "integrity": "sha512-Ea9N8KIU47lHeUzGI3qnRCtgqFv3z/tlgXc9HYgsBNA/H+GY5sadRG3h8G4bz46xEJPnaCW8bT0HwXyXEee39w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-1.2.1.tgz", + "integrity": "sha512-4sFIHpSC0MaVgJYseKypMdb+qUsVjxnILmissTpYkKTRvtAUq2C/UkKlMUuNQMX4jt+Os6CRWjat4+G5vzkb0w==", "requires": { "debug": "~4.1.1", "underscore": "^1.9.1" diff --git a/package.json b/package.json index 5bb15bad9..cffb48a49 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "cloudron-manifestformat": "^2.15.0", "connect": "^3.7.0", "connect-ensure-login": "^0.1.1", - "connect-lastmile": "^1.1.1", + "connect-lastmile": "^1.2.1", "connect-timeout": "^1.9.0", "cookie-parser": "^1.4.4", "cookie-session": "^1.3.3", diff --git a/src/apps.js b/src/apps.js index e4dd43966..6a47726ba 100644 --- a/src/apps.js +++ b/src/apps.js @@ -42,6 +42,7 @@ exports = module.exports = { getAppConfig: getAppConfig, getDataDir: getDataDir, + getIconPath: getIconPath, downloadFile: downloadFile, uploadFile: uploadFile, @@ -142,7 +143,7 @@ function AppsError(reason, errorOrMessage, details) { this.nestedError = errorOrMessage; } - if (details) _.extend(this, details); + if (details) error.details = details; } util.inherits(AppsError, Error); AppsError.INTERNAL_ERROR = 'Internal Error'; @@ -199,7 +200,7 @@ function validatePortBindings(portBindings, manifest) { const hostPort = portBindings[portName]; if (!Number.isInteger(hostPort)) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not an integer`); - if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(hostPort)); + if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new AppsError(AppsError.PORT_RESERVED, `Port ${hostPort} is reserved.`); if (hostPort <= 1023 || hostPort > 65535) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not in permitted range`); } @@ -379,7 +380,7 @@ function getDuplicateErrorDetails(errorMessage, location, domainObject, portBind // check if any of the port bindings conflict for (let portName in portBindings) { - if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]); + if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, `Port ${match[1]} is reserved`); } return new AppsError(AppsError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`); @@ -436,6 +437,22 @@ function getIconUrlSync(app) { return null; } +function getIconPath(appId, options, callback) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof callback, 'function'); + + if (!options.original) { + const userIconPath = `${paths.APP_ICONS_DIR}/${appId}.user.png`; + if (safe.fs.existsSync(userIconPath)) return callback(null, userIconPath); + } + + const appstoreIconPath = `${paths.APP_ICONS_DIR}/${appId}.png`; + if (safe.fs.existsSync(appstoreIconPath)) return callback(null, appstoreIconPath); + + callback(new AppsError(AppsError.NOT_FOUND, 'No icon')); +} + function postProcess(app, domainObjectMap) { let result = {}; for (let portName in app.portBindings) { diff --git a/src/routes/apps.js b/src/routes/apps.js index 6ad9028b4..ab21c0d36 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -37,12 +37,32 @@ var apps = require('../apps.js'), util = require('util'), WebSocket = require('ws'); +function toHttpError(appError) { + switch (appError.reason) { + case AppsError.NOT_FOUND: + return new HttpError(404, appError); + case AppsError.ALREADY_EXISTS: + case AppsError.PORT_RESERVED: + case AppsError.PORT_CONFLICT: + return new HttpError(409, appError); + case AppsError.BAD_FIELD: + case AppsError.BAD_CERTIFICATE: + return new HttpError(400, appError); + case AppsError.PLAN_LIMIT: + return new HttpError(402, appError); + case AppsError.EXTERNAL_ERROR: + return new HttpError(424, appError); + case AppsError.INTERNAL_ERROR: + default: + return new HttpError(500, appError); + } +} + function getApp(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); apps.get(req.params.id, function (error, app) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); next(new HttpSuccess(200, apps.removeInternalFields(app))); }); @@ -52,7 +72,7 @@ function getApps(req, res, next) { assert.strictEqual(typeof req.user, 'object'); apps.getAllByUser(req.user, function (error, allApps) { - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); allApps = allApps.map(apps.removeRestrictedFields); @@ -63,15 +83,11 @@ function getApps(req, res, next) { function getAppIcon(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); - if (!req.query.original) { - const userIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.user.png`; - if (safe.fs.existsSync(userIconPath)) return res.sendFile(userIconPath); - } + apps.getIconPath(req.params.id, { original: req.query.original }, function (error, iconPath) { + if (error) return next(toHttpError(error)); - const appstoreIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.png`; - if (safe.fs.existsSync(appstoreIconPath)) return res.sendFile(appstoreIconPath); - - return next(new HttpError(404, 'No such icon')); + res.sendFile(iconPath); + }); } function installApp(req, res, next) { @@ -127,15 +143,7 @@ function installApp(req, res, next) { debug('Installing app :%j', data); apps.install(data, req.user, auditSource.fromRequest(req), function (error, result) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message)); - if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message)); - if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.')); - if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.')); - if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message)); - if (error && error.reason === AppsError.PLAN_LIMIT) return next(new HttpError(402, error.message)); - if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message)); - if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { id: result.id, taskId: result.taskId })); }); @@ -190,14 +198,7 @@ function configureApp(req, res, next) { debug('Configuring app id:%s data:%j', req.params.id, data); apps.configure(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) { - if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message)); - if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.')); - if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.')); - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message)); - if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); @@ -215,11 +216,7 @@ function restoreApp(req, res, next) { if (data.backupId !== null && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null')); apps.restore(req.params.id, data, auditSource.fromRequest(req), function (error, result) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message)); - if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); @@ -239,16 +236,7 @@ function cloneApp(req, res, next) { if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object')); apps.clone(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.')); - if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.')); - if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message)); - if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message)); - if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error && error.reason === AppsError.PLAN_LIMIT) return next(new HttpError(402, error.message)); - if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message)); - if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); next(new HttpSuccess(201, { id: result.id, taskId: result.taskId })); }); @@ -260,10 +248,7 @@ function backupApp(req, res, next) { debug('Backup app id:%s', req.params.id); apps.backup(req.params.id, function (error, result) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); @@ -275,10 +260,7 @@ function uninstallApp(req, res, next) { debug('Uninstalling app id:%s', req.params.id); apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) { - if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error)); - if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); @@ -290,9 +272,7 @@ function startApp(req, res, next) { debug('Start app id:%s', req.params.id); apps.start(req.params.id, function (error) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { })); }); @@ -304,9 +284,7 @@ function stopApp(req, res, next) { debug('Stop app id:%s', req.params.id); apps.stop(req.params.id, function (error) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { })); }); @@ -328,10 +306,7 @@ function updateApp(req, res, next) { debug('Update app id:%s to manifest:%j', req.params.id, data.manifest); apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message)); - if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); @@ -357,9 +332,7 @@ function getLogStream(req, res, next) { }; apps.getLogs(req.params.id, options, function (error, logStream) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); res.writeHead(200, { 'Content-Type': 'text/event-stream', @@ -394,9 +367,7 @@ function getLogs(req, res, next) { }; apps.getLogs(req.params.id, options, function (error, logStream) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); res.writeHead(200, { 'Content-Type': 'application/x-logs', @@ -450,9 +421,7 @@ function exec(req, res, next) { var tty = req.query.tty === 'true' ? true : false; apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade')); @@ -492,9 +461,7 @@ function execWebSocket(req, res, next) { var tty = req.query.tty === 'true' ? true : false; apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); debug('Connected to terminal'); @@ -534,8 +501,7 @@ function listBackups(req, res, next) { if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number')); apps.listBackups(page, perPage, req.params.id, function (error, result) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); next(new HttpSuccess(200, { backups: result })); }); @@ -550,8 +516,7 @@ function uploadFile(req, res, next) { if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart')); apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); debug('uploadFile: done'); @@ -567,8 +532,7 @@ function downloadFile(req, res, next) { if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided')); apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) { - if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message)); - if (error) return next(new HttpError(500, error)); + if (error) return next(toHttpError(error)); var headers = { 'Content-Type': 'application/octet-stream',