diff --git a/src/apps.js b/src/apps.js index 904c7b023..bbd4a6e29 100644 --- a/src/apps.js +++ b/src/apps.js @@ -133,6 +133,7 @@ exports = module.exports = { HEALTH_DEAD: 'dead', // exported for testing + _checkForPortBindingConflict: checkForPortBindingConflict, _validatePortBindings: validatePortBindings, _validateAccessRestriction: validateAccessRestriction, _validateUpstreamUri: validateUpstreamUri, @@ -795,6 +796,39 @@ function accessLevel(app, user) { return canAccess(app, user) ? 'user' : null; } +async function checkForPortBindingConflict(portBindings, id = '') { + assert.strictEqual(typeof portBindings, 'object'); + assert.strictEqual(typeof id, 'string'); + + let existingPortBindings; + if (id) existingPortBindings = await database.query('SELECT * FROM appPortBindings WHERE appId != ?', [ id ]); + else existingPortBindings = await database.query('SELECT * FROM appPortBindings', []); + + if (existingPortBindings.length === 0) return; + + const tcpPorts = existingPortBindings.filter((p) => p.type === 'tcp'); + const udpPorts = existingPortBindings.filter((p) => p.type === 'udp'); + + for (let portName in portBindings) { + const p = portBindings[portName]; + const testPorts = p.type === 'tcp' ? tcpPorts : udpPorts; + + const found = testPorts.find((e) => { + // if one is true we dont have a conflict + // a1 <----> a2 b1 <-------> b2 + // b1 <------> b2 a1 <-----> a2 + const a2 = (e.hostPort + e.count - 1); + const b1 = p.hostPort; + const b2 = (p.hostPort + p.portCount -1); + const a1 = e.hostPort; + + return !((a2 < b1) || (b2 < a1)); + }); + + if (found) throw new BoxError(BoxError.CONFLICT, `Conflicting port ${p.hostPort}`); + } +} + async function add(id, appStoreId, manifest, subdomain, domain, portBindings, data) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof appStoreId, 'string'); @@ -830,6 +864,8 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da enableRedis = 'enableRedis' in data ? data.enableRedis : true, icon = data.icon || null; + await checkForPortBindingConflict(portBindings); + const queries = []; queries.push({ @@ -914,10 +950,14 @@ async function updateWithConstraints(id, app, constraints) { assert(!('tags' in app) || Array.isArray(app.tags)); assert(!('env' in app) || typeof app.env === 'object'); + const queries = [ ]; if ('portBindings' in app) { const portBindings = app.portBindings || { }; + + await checkForPortBindingConflict(portBindings, id); + // replace entries by app id queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] }); Object.keys(portBindings).forEach(function (env) { diff --git a/src/test/apps-test.js b/src/test/apps-test.js index 2dd06c96d..9398ca0bc 100644 --- a/src/test/apps-test.js +++ b/src/test/apps-test.js @@ -19,6 +19,39 @@ describe('Apps', function () { before(domainSetup); after(cleanup); + describe('checkForPortBindingConflict', function () { + before(async function () { + await apps.add(app.id, app.appStoreId, app.manifest, app.subdomain, app.domain, [{ hostPort: 40000, type: 'tcp', portCount: 100 }, { hostPort: 50000, type: 'udp', portCount: 1 }], app); + }); + + after(async function () { + await apps.del(app.id); + }); + + it('throws on exact conflict', async function () { + let [error] = await safe(apps._checkForPortBindingConflict([{ hostPort: 40000, type: 'tcp', portCount: 1 }])); + expect(error.reason).to.equal(BoxError.CONFLICT); + + [error] = await safe(apps._checkForPortBindingConflict([{ hostPort: 50000, type: 'udp', portCount: 1 }])); + expect(error.reason).to.equal(BoxError.CONFLICT); + }); + + it('throws on range conflict', async function () { + let [error] = await safe(apps._checkForPortBindingConflict([{ hostPort: 40080, type: 'tcp', portCount: 40 }])); + expect(error.reason).to.equal(BoxError.CONFLICT); + + [error] = await safe(apps._checkForPortBindingConflict([{ hostPort: 49995, type: 'udp', portCount: 20 }])); + expect(error.reason).to.equal(BoxError.CONFLICT); + }); + + it('succeeds without conflict', async function () { + await apps._checkForPortBindingConflict([{ hostPort: 39995, type: 'tcp', portCount: 2 }]); + await apps._checkForPortBindingConflict([{ hostPort: 45000, type: 'tcp', portCount: 1 }]); + await apps._checkForPortBindingConflict([{ hostPort: 49995, type: 'udp', portCount: 2 }]); + await apps._checkForPortBindingConflict([{ hostPort: 50001, type: 'udp', portCount: 1 }]); + }); + }); + describe('validateLocations', function () { it('does not allow reserved subdomain', async function () { let location = new Location('my', domain.domain, Location.TYPE_ALIAS);