diff --git a/test/fixtures/server/filters.json b/test/fixtures/server/filters.json index 9446b0b83..33fba26d8 100644 --- a/test/fixtures/server/filters.json +++ b/test/fixtures/server/filters.json @@ -3,50 +3,50 @@ { "path": "/echo-param/:param", "method": "GET", - "origin": "http://localhost:${originPort}" + "origin": "http://localhost:9000" }, { "path": "/echo-body/:param?", "method": "POST", - "origin": "http://localhost:${originPort}" + "origin": "http://localhost:9000" }, { "path": "/echo-headers/:param?", "method": "POST", - "origin": "http://localhost:${originPort}" + "origin": "http://localhost:9000" }, { "path": "/echo-query", "method": "GET", - "origin": "http://localhost:${originPort}" + "origin": "http://localhost:9000" }, { "path": "/echo-query/:param?", "method": "GET", - "origin": "http://localhost:${originPort}" + "origin": "http://localhost:9000" }, { "path": "/test-blob/*", "method": "GET", - "origin": "http://localhost:${originPort}" + "origin": "http://localhost:9000" }, { "path": "/test-blob-param/*", "method": "GET", - "origin": "http://localhost:${originPort}" + "origin": "http://localhost:9000" }, { "//": "Block on headers", "path": "/echo-param-protected/:param", "method": "GET", - "origin": "http://localhost:${originPort}", + "origin": "http://localhost:9000", "valid": [ { "header": "accept", diff --git a/test/functional/client-server.test.ts b/test/functional/client-server.test.ts index ced2af5f3..5a7f2f861 100644 --- a/test/functional/client-server.test.ts +++ b/test/functional/client-server.test.ts @@ -1,275 +1,334 @@ -// const t = require('tap'); -// const path = require('path'); -// const request = require('request'); -// const app = require('../../lib'); -// const { port, createTestServer } = require('../utils'); -// const root = __dirname; +// noinspection DuplicatedCode + +import * as path from 'path'; +import axios from 'axios'; +import { BrokerClient, createBrokerClient } from '../setup/broker-client'; +import { BrokerServer, createBrokerServer } from '../setup/broker-server'; +import { TestWebServer, createTestWebServer } from '../setup/test-web-server'; + +const fixtures = path.resolve(__dirname, '..', 'fixtures'); +const serverAccept = path.join(fixtures, 'server', 'filters.json'); +const clientAccept = path.join(fixtures, 'client', 'filters.json'); describe('proxy requests originating from behind the broker client', () => { - it.skip('server identifies self to client', async () => {}); -}); + let tws: TestWebServer; + let bs: BrokerServer; + let bc: BrokerClient; + let brokerToken: string; + let serverMetadata: unknown; + + beforeAll(async () => { + tws = await createTestWebServer(); + + bs = await createBrokerServer({ filters: serverAccept }); + + bc = await createBrokerClient({ + brokerServerUrl: `http://localhost:${bs.port}`, + brokerToken: 'broker-token-12345', + filters: clientAccept, + type: 'client', + }); + + await new Promise((resolve) => { + bs.server.io.on('connection', (socket) => { + socket.on('identify', (clientData) => { + brokerToken = clientData.token; + resolve(brokerToken); + }); + }); + }); + }); + + afterAll(async () => { + await tws.server.close(); + setTimeout(async () => { + await bc.client.close(); + }, 100); + await new Promise((resolve) => { + bc.client.io.on('close', () => { + resolve(); + }); + }); + + setTimeout(async () => { + await bs.server.close(); + }, 100); + await new Promise((resolve) => { + bs.server.io.on('close', () => { + resolve(); + }); + }); + }); + + it('server identifies self to client', async () => { + await new Promise((resolve) => { + bc.client.io.on('identify', (serverData) => { + serverMetadata = serverData; + resolve(serverData); + }); + }); + + expect(brokerToken).toEqual('broker-token-12345'); + expect(serverMetadata).toMatchObject({ + capabilities: ['receive-post-streams'], + }); + }); + + it('successfully broker POST', async () => { + const response = await axios.post( + `http://localhost:${bc.port}/echo-body`, + { some: { example: 'json' } }, + { + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(200); + expect(response.data).toStrictEqual({ + some: { example: 'json' }, + }); + }); + + it('successfully broker exact bytes of POST body', async () => { + // stringify the JSON unusually to ensure an unusual exact body + const body = Buffer.from( + JSON.stringify({ some: { example: 'json' } }, null, 5), + ); + const response = await axios.post( + `http://localhost:${bc.port}/echo-body`, + body, + { + headers: { 'content-type': 'application/json' }, + timeout: 1000, + transformResponse: (r) => r, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(200); + expect(Buffer.from(response.data)).toEqual(body); + }); + + it('successfully broker GET', async () => { + const response = await axios.get( + `http://localhost:${bc.port}/echo-param/xyz`, + { + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(200); + expect(response.data).toEqual('xyz'); + }); + + it('block request for non-whitelisted url', async () => { + const response = await axios.post( + `http://localhost:${bc.port}/not-allowed`, + {}, + { + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(401); + expect(response.data).toStrictEqual({ + message: 'blocked', + reason: 'Request does not match any accept rule, blocking HTTP request', + url: '/not-allowed', + }); + }); + + it('allow request for valid url with valid body', async () => { + const response = await axios.post( + `http://localhost:${bc.port}/echo-body/filtered`, + { proxy: { me: 'please' } }, + { + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(200); + expect(response.data).toStrictEqual({ + proxy: { me: 'please' }, + }); + }); -// t.test( -// 'proxy requests originating from behind the broker client', -// async (t) => { -// /** -// * 1. start broker in server mode -// * 2. start broker in client mode and join (1) -// * 3. run local http server that replicates "private server" -// * 4. send requests to **client** -// * -// * Note: server is forwarding requests to echo-server defined in test/util.js -// */ -// -// const { echoServerPort, testServer } = createTestServer(); -// -// t.teardown(() => { -// testServer.close(); -// }); -// -// process.env.ACCEPT = 'filters.json'; -// -// process.chdir(path.resolve(root, '../fixtures/server')); -// process.env.BROKER_TYPE = 'server'; -// process.env.ORIGIN_PORT = echoServerPort; -// const serverPort = port(); -// const server = await app.main({ port: serverPort }); -// -// process.chdir(path.resolve(root, '../fixtures/client')); -// process.env.BROKER_TYPE = 'client'; -// process.env.BROKER_TOKEN = 'C481349B-4014-43D9-B59D-BA41E1315001'; // uuid.v4 -// process.env.BROKER_SERVER_URL = `http://localhost:${serverPort}`; -// const clientPort = port(); -// const client = await app.main({ port: clientPort }); -// -// t.plan(17); -// -// client.io.once('identify', (serverData) => { -// t.test('server identifies self to client', (t) => { -// t.same( -// serverData, -// { capabilities: ['receive-post-streams'] }, -// 'server advertises capabilities', -// ); -// t.end(); -// }); -// }); -// -// // wait for the client to successfully connect to the server and identify itself -// server.io.once('connection', (socket) => { -// socket.once('identify', (clientData) => { -// t.test('successfully broker POST', (t) => { -// const url = `http://localhost:${clientPort}/echo-body`; -// const body = { some: { example: 'json' } }; -// request({ url, method: 'post', json: true, body }, (err, res) => { -// t.equal(res.statusCode, 200, '200 statusCode'); -// t.same(res.body, body, 'body brokered'); -// t.end(); -// }); -// }); -// -// t.test('successfully broker exact bytes of POST body', (t) => { -// const url = `http://localhost:${clientPort}/echo-body`; -// // stringify the JSON unusually to ensure an unusual exact body -// const body = Buffer.from( -// JSON.stringify({ some: { example: 'json' } }, null, 5), -// ); -// const headers = { 'Content-Type': 'application/json' }; -// request({ url, method: 'post', headers, body }, (err, res) => { -// const responseBody = Buffer.from(res.body); -// t.equal(res.statusCode, 200, '200 statusCode'); -// t.same(responseBody, body, 'body brokered exactly'); -// t.end(); -// }); -// }); -// -// t.test('successfully broker GET', (t) => { -// const url = `http://localhost:${clientPort}/echo-param/xyz`; -// request({ url, method: 'get' }, (err, res) => { -// t.equal(res.statusCode, 200, '200 statusCode'); -// t.equal(res.body, 'xyz', 'body brokered'); -// t.end(); -// }); -// }); -// -// // the filtering happens in the broker client -// t.test('block request for non-whitelisted url', (t) => { -// const url = `http://localhost:${clientPort}/not-allowed`; -// request({ url, method: 'post', json: true }, (err, res, body) => { -// t.equal(res.statusCode, 401, '401 statusCode'); -// t.equal(body.message, 'blocked', '"blocked" body: ' + body); -// t.equal( -// body.reason, -// 'Request does not match any accept rule, blocking HTTP request', -// 'Block message', -// ); -// t.equal(body.url, '/not-allowed', 'Blocked url'); -// t.end(); -// }); -// }); -// -// // the filtering happens in the broker client -// t.test('allow request for valid url with valid body', (t) => { -// const url = `http://localhost:${clientPort}/echo-body/filtered`; -// const body = { proxy: { me: 'please' } }; -// request({ url, method: 'post', json: true, body }, (err, res) => { -// t.equal(res.statusCode, 200, '200 statusCode'); -// t.same(res.body, body, 'body brokered'); -// t.end(); -// }); -// }); -// -// // the filtering happens in the broker client -// t.test('block request for valid url with invalid body', (t) => { -// const url = `http://localhost:${clientPort}/echo-body/filtered`; -// const body = { proxy: { me: 'now!' } }; -// request( -// { url, method: 'post', json: true, body }, -// (err, res, body) => { -// t.equal(res.statusCode, 401, '401 statusCode'); -// t.equal(body.message, 'blocked', '"blocked" body: ' + body); -// t.end(); -// }, -// ); -// }); -// -// // the filtering happens in the broker client -// t.test('allow request for valid url with valid query param', (t) => { -// const url = `http://localhost:${clientPort}/echo-query/filtered`; -// const qs = { proxyMe: 'please' }; -// request({ url, method: 'get', json: true, qs }, (err, res) => { -// t.equal(res.statusCode, 200, '200 statusCode'); -// t.same(res.body, qs, 'querystring brokered'); -// t.end(); -// }); -// }); -// -// // the filtering happens in the broker client -// t.test('block request for valid url with invalid query param', (t) => { -// const url = `http://localhost:${clientPort}/echo-query/filtered`; -// const qs = { proxyMe: 'now!' }; -// request({ url, method: 'get', qs }, (err, res) => { -// t.equal(res.statusCode, 401, '401 statusCode'); -// t.end(); -// }); -// }); -// -// // the filtering happens in the broker client -// t.test('block request for valid url with missing query param', (t) => { -// const url = `http://localhost:${clientPort}/echo-query/filtered`; -// request({ url, method: 'get' }, (err, res) => { -// t.equal(res.statusCode, 401, '401 statusCode'); -// t.end(); -// }); -// }); -// -// // the filtering happens in the broker server -// t.test( -// 'block request for valid URL which is not allowed on server', -// (t) => { -// const url = `http://localhost:${clientPort}/server-side-blocked`; -// request({ url, method: 'get' }, (err, res) => { -// t.equal(res.statusCode, 401, '401 statusCode'); -// t.end(); -// }); -// }, -// ); -// -// // the filtering happens in the broker server - this indicates a very badly misconfigured client -// t.test( -// 'block request for valid URL which is not allowed on server with streaming response', -// (t) => { -// const url = `http://localhost:${clientPort}/server-side-blocked-streaming`; -// request({ url, method: 'get' }, (err, res) => { -// t.equal(res.statusCode, 401, '401 statusCode'); -// t.end(); -// }); -// }, -// ); -// -// t.test('allow request for valid url with valid accept header', (t) => { -// const url = `http://localhost:${clientPort}/echo-param-protected/xyz`; -// request( -// { -// url, -// method: 'get', -// headers: { -// ACCEPT: 'valid.accept.header', -// accept: 'valid.accept.header', -// }, -// }, -// (err, res) => { -// t.equal(res.statusCode, 200, '200 statusCode'); -// t.equal(res.body, 'xyz', 'body brokered'); -// t.end(); -// }, -// ); -// }); -// -// t.test( -// 'block request for valid url with invalid accept header', -// (t) => { -// const invalidAcceptHeader = 'invalid.accept.header'; -// const url = `http://localhost:${clientPort}/echo-param-protected/xyz`; -// request( -// { -// url, -// method: 'get', -// headers: { -// ACCEPT: invalidAcceptHeader, -// accept: invalidAcceptHeader, -// }, -// }, -// (err, res) => { -// t.equal(res.statusCode, 401, '401 statusCode'); -// t.end(); -// }, -// ); -// }, -// ); -// -// // this validates that the broker *server* sends to the correct broker token -// // header to the echo-server -// t.test( -// 'broker ID is included in headers from server to private', -// (t) => { -// const url = `http://localhost:${clientPort}/echo-headers`; -// request({ url, method: 'post' }, (err, res) => { -// const responseBody = JSON.parse(res.body); -// t.equal(res.statusCode, 200, '200 statusCode'); -// t.equal( -// responseBody['x-broker-token'], -// clientData.token.toLowerCase(), -// 'X-Broker-Token header present and lowercased', -// ); -// t.end(); -// }); -// }, -// ); -// -// t.test('querystring parameters are brokered', (t) => { -// const url = `http://localhost:${clientPort}/echo-query?shape=square&colour=yellow`; -// request({ url, method: 'get' }, (err, res) => { -// const responseBody = JSON.parse(res.body); -// t.equal(res.statusCode, 200, '200 statusCode'); -// t.same( -// responseBody, -// { shape: 'square', colour: 'yellow' }, -// 'querystring brokered', -// ); -// t.end(); -// }); -// }); -// -// t.test('clean up', (t) => { -// client.close(); -// setTimeout(() => { -// server.close(); -// t.ok('sockets closed'); -// t.end(); -// }, 100); -// }); -// }); -// }); -// }, -// ); + it('block request for valid url with invalid body', async () => { + const response = await axios.post( + `http://localhost:${bc.port}/echo-body/filtered`, + { proxy: { me: 'now!' } }, + { + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(401); + expect(response.data).toStrictEqual({ + message: 'blocked', + reason: 'Request does not match any accept rule, blocking HTTP request', + url: '/echo-body/filtered', + }); + }); + + it('allow request for valid url with valid query param', async () => { + const response = await axios.get( + `http://localhost:${bc.port}/echo-query/filtered`, + { + params: { proxyMe: 'please' }, + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(200); + expect(response.data).toStrictEqual({ + proxyMe: 'please', + }); + }); + + it('block request for valid url with invalid query param', async () => { + const response = await axios.get( + `http://localhost:${bc.port}/echo-query/filtered`, + { + params: { proxyMe: 'now!' }, + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(401); + expect(response.data).toStrictEqual({ + message: 'blocked', + reason: 'Request does not match any accept rule, blocking HTTP request', + url: '/echo-query/filtered?proxyMe=now!', + }); + }); + + it('block request for valid url with missing query param', async () => { + const response = await axios.get( + `http://localhost:${bc.port}/echo-query/filtered`, + { + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(401); + expect(response.data).toStrictEqual({ + message: 'blocked', + reason: 'Request does not match any accept rule, blocking HTTP request', + url: '/echo-query/filtered', + }); + }); + + it('block request for valid URL which is not allowed on server', async () => { + const response = await axios.get( + `http://localhost:${bc.port}/server-side-blocked`, + { + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(401); + expect(response.data).toStrictEqual({ + message: 'blocked', + reason: + 'Response does not match any accept rule, blocking websocket request', + url: 'http://localhost:9000/server-side-blocked', + }); + }); + + it('block request for valid URL which is not allowed on server with streaming response', async () => { + const response = await axios.get( + `http://localhost:${bc.port}/server-side-blocked-streaming`, + { + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(401); + expect(response.data).toStrictEqual({ + message: 'blocked', + reason: + 'Response does not match any accept rule, blocking websocket request', + url: 'http://localhost:9000/server-side-blocked-streaming', + }); + }); + + it('allow request for valid url with valid accept header', async () => { + const response = await axios.get( + `http://localhost:${bc.port}/echo-param-protected/xyz`, + { + headers: { + ACCEPT: 'valid.accept.header', + accept: 'valid.accept.header', + }, + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(200); + expect(response.data).toEqual('xyz'); + }); + + it('block request for valid url with invalid accept header', async () => { + const response = await axios.get( + `http://localhost:${bc.port}/echo-param-protected/xyz`, + { + headers: { + ACCEPT: 'invalid.accept.header', + accept: 'invalid.accept.header', + }, + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(401); + expect(response.data).toStrictEqual({ + message: 'blocked', + reason: 'Request does not match any accept rule, blocking HTTP request', + url: '/echo-param-protected/xyz', + }); + }); + + it('broker ID is included in headers from server to private', async () => { + const response = await axios.post( + `http://localhost:${bc.port}/echo-headers`, + {}, + { + timeout: 1000, + validateStatus: () => true, + }, + ); + + expect(response.status).toEqual(200); + expect(response.data).toHaveProperty('x-broker-token'); + expect(response.data['x-broker-token']).toEqual(brokerToken); + }); + + it('querystring parameters are brokered', async () => { + const response = await axios.get(`http://localhost:${bc.port}/echo-query`, { + params: { + shape: 'square', + colour: 'yellow', + }, + timeout: 1000, + validateStatus: () => true, + }); + + expect(response.status).toEqual(200); + expect(response.data).toStrictEqual({ + shape: 'square', + colour: 'yellow', + }); + }); +});