diff --git a/src/appstore.js b/src/appstore.js index 33d12ec1f..d7d752cf5 100644 --- a/src/appstore.js +++ b/src/appstore.js @@ -45,6 +45,17 @@ var apps = require('./apps.js'), const NOOP_CALLBACK = function (error) { if (error) debug(error); }; +function isAppAllowed(appstoreId, listingConfig) { + assert.strictEqual(typeof listingConfig, 'object'); + assert.strictEqual(typeof appstoreId, 'string'); + + if (listingConfig.blacklist && listingConfig.blacklist.includes(appstoreId)) return false; + + if (listingConfig.whitelist) return listingConfig.whitelist.includes(appstoreId); + + return true; +} + function getCloudronToken(callback) { assert.strictEqual(typeof callback, 'function'); @@ -503,7 +514,13 @@ function getApps(callback) { if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body))); if (!result.body.apps) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text))); - callback(null, result.body.apps); + settings.getAppstoreListingConfig(function (error, listingConfig) { + if (error) return callback(error); + + const filteredApps = result.body.apps.filter(app => isAppAllowed(app.id, listingConfig)); + + callback(null, filteredApps); + }); }); }); }); @@ -514,20 +531,26 @@ function getAppVersion(appId, version, callback) { assert.strictEqual(typeof version, 'string'); assert.strictEqual(typeof callback, 'function'); - getCloudronToken(function (error, token) { + settings.getAppstoreListingConfig(function (error, listingConfig) { if (error) return callback(error); - let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`; - if (version !== 'latest') url += `/versions/${version}`; + if (!isAppAllowed(appId, listingConfig)) return callback(new BoxError(BoxError.FEATURE_DISABLED)); - superagent.get(url).query({ accessToken: token }).timeout(10 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); - if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message)); - if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', result.status, result.body))); + getCloudronToken(function (error, token) { + if (error) return callback(error); - callback(null, result.body); + let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`; + if (version !== 'latest') url += `/versions/${version}`; + + superagent.get(url).query({ accessToken: token }).timeout(10 * 1000).end(function (error, result) { + if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); + if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); + if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); + if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message)); + if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', result.status, result.body))); + + callback(null, result.body); + }); }); }); } diff --git a/src/boxerror.js b/src/boxerror.js index 34714834e..f300f5194 100644 --- a/src/boxerror.js +++ b/src/boxerror.js @@ -45,6 +45,7 @@ BoxError.DATABASE_ERROR = 'Database Error'; BoxError.DNS_ERROR = 'DNS Error'; BoxError.DOCKER_ERROR = 'Docker Error'; BoxError.EXTERNAL_ERROR = 'External Error'; // use this for external API errors +BoxError.FEATURE_DISABLED = 'Feature Disabled'; BoxError.FS_ERROR = 'FileSystem Error'; BoxError.INACTIVE = 'Inactive'; BoxError.INTERNAL_ERROR = 'Internal Error'; @@ -77,6 +78,8 @@ BoxError.toHttpError = function (error) { return new HttpError(402, error); case BoxError.NOT_FOUND: return new HttpError(404, error); + case BoxError.FEATURE_DISABLED: + return new HttpError(405, error); case BoxError.ALREADY_EXISTS: case BoxError.BAD_STATE: case BoxError.CONFLICT: diff --git a/src/settings.js b/src/settings.js index 6583321d2..085bd5d4b 100644 --- a/src/settings.js +++ b/src/settings.js @@ -49,6 +49,8 @@ exports = module.exports = { getFooter: getFooter, setFooter: setFooter, + getAppstoreListingConfig: getAppstoreListingConfig, + provider: provider, getAll: getAll, @@ -78,6 +80,7 @@ exports = module.exports = { EXTERNAL_LDAP_KEY: 'external_ldap_config', REGISTRY_CONFIG_KEY: 'registry_config', SYSINFO_CONFIG_KEY: 'sysinfo_config', + APPSTORE_LISTING_CONFIG_KEY: 'appstore_listing_config', // strings APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern', @@ -155,6 +158,11 @@ let gDefaults = (function () { result[exports.WEB_SERVER_ORIGIN_KEY] = 'https://cloudron.io'; result[exports.DEMO_KEY] = false; + result[exports.APPSTORE_LISTING_CONFIG_KEY] = { + blacklist: [], + whitelist: null // null imples, not set. this is an object and not an array + }; + result[exports.FOOTER_KEY] = '© 2020 [Cloudron](https://cloudron.io) [Forum ](https://forum.cloudron.io)'; return result; @@ -520,6 +528,17 @@ function setSysinfoConfig(sysinfoConfig, callback) { }); } +function getAppstoreListingConfig(callback) { + assert.strictEqual(typeof callback, 'function'); + + settingsdb.get(exports.APPSTORE_LISTING_CONFIG_KEY, function (error, value) { + if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.APPSTORE_LISTING_CONFIG_KEY]); + if (error) return callback(error); + + callback(null, JSON.parse(value)); + }); +} + function getLicenseKey(callback) { assert.strictEqual(typeof callback, 'function');