diff --git a/src/apps.js b/src/apps.js index 69f8c546c..e0a58dfd8 100644 --- a/src/apps.js +++ b/src/apps.js @@ -146,6 +146,7 @@ exports = module.exports = { // exported for testing _validatePortBindings: validatePortBindings, _validateAccessRestriction: validateAccessRestriction, + _validateUpstreamUri: validateUpstreamUri, _translatePortBindings: translatePortBindings, _parseCrontab: parseCrontab, _clear: clear diff --git a/src/routes/test/apps-test.js b/src/routes/test/apps-test.js index e05acc95d..9f76ad428 100644 --- a/src/routes/test/apps-test.js +++ b/src/routes/test/apps-test.js @@ -854,6 +854,37 @@ xdescribe('App API', function () { }); }); + ////////////// upstreamUri + it('cannot set empty upstreamUri', function (done) { + superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/upstream_uri') + .query({ access_token: token }) + .send({ upstreamUri: '' }) + .end(function (err, res) { + expect(res.statusCode).to.equal(400); + done(); + }); + }); + + it('cannot set bad upstreamUri', function (done) { + superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/upstream_uri') + .query({ access_token: token }) + .send({ upstreamUri: 'foobar:com' }) + .end(function (err, res) { + expect(res.statusCode).to.equal(400); + done(); + }); + }); + + it('can set upstreamUri', function (done) { + superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/upstream_uri') + .query({ access_token: token }) + .send({ upstreamUri: 'https://1.2.3.4:443' }) + .end(function (err, res) { + expect(res.statusCode).to.equal(200); + done(); + }); + }); + /////////////// cert it('cannot set only the cert, no key', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/cert') diff --git a/src/test/apps-test.js b/src/test/apps-test.js index e282a0d01..5cc2dc793 100644 --- a/src/test/apps-test.js +++ b/src/test/apps-test.js @@ -9,6 +9,7 @@ const apps = require('../apps.js'), AuditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), common = require('./common.js'), + constants = require('../constants.js'), expect = require('expect.js'), safe = require('safetydance'); @@ -72,6 +73,40 @@ describe('Apps', function () { }); }); + describe('validateUpstreamUri', function () { + it('does not allow empty URI', function () { + expect(apps._validateUpstreamUri('')).to.be.an(Error); + }); + + it('does not allow invalid URI scheme', function () { + expect(apps._validateUpstreamUri('bla:blub')).to.be.an(Error); + }); + + it('does not allow unsupported scheme', function () { + expect(apps._validateUpstreamUri('ftp://foobar.com')).to.be.an(Error); + }); + + it('does not allow trailing URI paths ', function () { + expect(apps._validateUpstreamUri('https://foobar.com/extra/path')).to.be.an(Error); + }); + + it('allows IP', function () { + expect(apps._validateUpstreamUri('http://1.2.3.4')).to.eql(null); + }); + + it('allows IP with port', function () { + expect(apps._validateUpstreamUri('http://1.2.3.4:80')).to.eql(null); + }); + + it('allows domain', function () { + expect(apps._validateUpstreamUri('https://www.cloudron.io')).to.eql(null); + }); + + it('allows domain with port', function () { + expect(apps._validateUpstreamUri('https://www.cloudron.io:443')).to.eql(null); + }); + }); + describe('canAccess', function () { const someuser = { id: 'someuser', groupIds: [], role: 'user' }; const adminuser = { id: 'adminuser', groupIds: [ 'groupie' ], role: 'admin' }; @@ -269,6 +304,27 @@ describe('Apps', function () { }); }); + describe('proxy app', function () { + const app = require('./common.js').proxyApp; + const newUpstreamUri = 'https://foobar.com:443'; + + before(async function () { + await apps.add(app.id, app.appStoreId, app.manifest, app.subdomain, app.domain, app.portBindings, app); + }); + + it('cannot set invalid upstream uri', async function () { + const [error] = await safe(apps.setUpstreamUri(app, 'foo:bar:80', AuditSource.PLATFORM)); + expect(error.reason).to.be(BoxError.BAD_FIELD); + }); + + it('can set upstream uri', async function () { + await apps.setUpstreamUri(app, newUpstreamUri, AuditSource.PLATFORM); + const result = await apps.get(app.id); + + expect(result.upstreamUri).to.equal(newUpstreamUri); + }); + }); + describe('configureInstalledApps', function () { const app1 = Object.assign({}, app, { id: 'id1', installationState: apps.ISTATE_ERROR, subdomain: 'loc1' }); const app2 = Object.assign({}, app, { id: 'id2', installationState: apps.ISTATE_INSTALLED, subdomain: 'loc2' }); diff --git a/src/test/common.js b/src/test/common.js index b7ccf7cc2..5e793444c 100644 --- a/src/test/common.js +++ b/src/test/common.js @@ -43,6 +43,34 @@ const manifest = { } }; +// copied from the proxy app CloudronManifest.json +const proxyAppManifest = { + "id": "io.cloudron.builtin.appproxy", + "title": "App Proxy", + "author": "Cloudron Team", + "version": "1.0.0", + "upstreamVersion": "1.0.0", + "description": "file://DESCRIPTION.md", + "tagline": "Proxy an app through Cloudron", + "tags": [ "proxy", "external" ], + "healthCheckPath": "/", + "httpPort": 3000, + "minBoxVersion": "7.3.0", + "dockerImage": "istobeignored", + "manifestVersion": 2, + "multiDomain": true, + "website": "https://cloudron.io", + "documentationUrl": "https://docs.cloudron.io/dashboard/#app-proxy", + "forumUrl": "https://forum.cloudron.io", + "contactEmail": "support@cloudron.io", + "icon": "file://logo.png", + "addons": {}, + "mediaLinks": [ + "https://screenshots.cloudron.io/io.cloudron.builtin.appproxy/diagram.png" + ], + "changelog": "file://CHANGELOG.md" +}; + const domain = { domain: 'example.com', zoneName: 'example.com', @@ -110,6 +138,27 @@ const app = { }; Object.freeze(app); +const proxyApp = { + id: 'proxyapptestid', + appStoreId: proxyAppManifest.id, + installationState: apps.ISTATE_PENDING_INSTALL, + runState: 'running', + subdomain: 'proxylocation', + upstreamUri: 'http://1.2.3.4:80', + domain: domain.domain, + fqdn: domain.domain + '.' + 'proxylocation', + manifest, + containerId: '', + portBindings: null, + accessRestriction: null, + memoryLimit: 0, + mailboxDomain: domain.domain, + secondaryDomains: [], + redirectDomains: [], + aliasDomains: [] +}; +Object.freeze(proxyApp); + exports = module.exports = { createTree, domainSetup, @@ -124,6 +173,7 @@ exports = module.exports = { dashboardFqdn: `my.${domain.domain}`, app, + proxyApp, admin, auditSource, domain, // the domain object