diff --git a/.gitleaks.toml b/.gitleaks.toml index f7a01f36f..290bf0743 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -11,4 +11,5 @@ commits = [ # false positive for pgp prefix and suffix blocks "555fe920c97af38fa78a875fbb858d5588a6ec80", "a8335efefaa9f483294554c5086702457b735651", + "0501dae214d6dc4d6fd574b57d06eee08042b18e" ] diff --git a/config.default.json b/config.default.json index f6a84e167..ad01f5058 100644 --- a/config.default.json +++ b/config.default.json @@ -1,6 +1,7 @@ { "BROKER_SERVER_UNIVERSAL_CONFIG_ENABLED": false, "SUPPORTED_BROKER_TYPES": [ + "apprisk", "artifactory", "azure-repos", "bitbucket-server", @@ -22,6 +23,37 @@ }, "required": {} }, + "apprisk": { + "validations": [ + { + "url": "https://$CHECKMARX/cxrestapi/auth/identity/connect/token", + "body": { + "BROKER_VAR_SUB": ["username", "password"], + "username": "$CHECKMARX_USERNAME", + "password": "$CHECKMARX_PASSWORD", + "grant_type": "password", + "scope": "sast_rest_api", + "client_id": "resource_owner_client", + "client_secret": "014DF517-39D1-4453-B7B3-9930C563627C" + }, + "method": "post", + "headers": { + "x-broker-content-type": "application/x-www-form-urlencoded" + } + } + ], + "default": { + "CHECKMARX": "$CHECKMARX", + "CHECKMARX_USERNAME": "$CHECKMARX_USERNAME", + "CHECKMARX_PASSWORD": "$CHECKMARX_PASSWORD" + }, + "required": { + "CHECKMARX": "checkmarx.customer.com", + "CHECKMARX_USERNAME": "", + "CHECKMARX_PASSWORD": "", + "BROKER_CLIENT_URL": "https://:" + } + }, "artifactory": { "validations": [ { @@ -280,6 +312,7 @@ } }, "FILTER_RULES_PATHS": { + "apprisk": "defaultFilters/apprisk.json", "artifactory": "defaultFilters/artifactory.json", "azure-repos": "defaultFilters/azure-repos.json", "bitbucket-server": "defaultFilters/bitbucket-server.json", @@ -427,6 +460,11 @@ "name": "Jira", "type": "jira", "brokerType": "jira-bearer-auth" + }, + "apprisk": { + "name": "Apprisk", + "type": "apprisk", + "brokerType": "apprisk" } } } diff --git a/defaultFilters/apprisk.json b/defaultFilters/apprisk.json new file mode 100644 index 000000000..2ae38928f --- /dev/null +++ b/defaultFilters/apprisk.json @@ -0,0 +1,36 @@ +{ + "public": [ + ], + "private": [ + { + "//": "Ask for Authentication token", + "method": "POST", + "path": "/cxrestapi/auth/identity/connect/token", + "origin": "https://${CHECKMARX}" + }, + { + "//": "Get All Project Details", + "method": "GET", + "path": "/projects", + "origin": "https://${CHECKMARX}" + }, + { + "//": "Get Remote Source Settings for GIT", + "method": "GET", + "path": "/projects/:id/sourceCode/remoteSettings/git", + "origin": "https://${CHECKMARX}" + }, + { + "//": "Get All Scans for Project", + "method": "GET", + "path": "/sast/scans", + "origin": "https://${CHECKMARX}" + }, + { + "//": "Get Statistic Results by Scan Id", + "method": "GET", + "path": "/sast/scans/:id/resultsStatistics", + "origin": "https://${CHECKMARX}" + } + ] + } diff --git a/lib/client/types/config.ts b/lib/client/types/config.ts index 5adf70577..9ce11e117 100644 --- a/lib/client/types/config.ts +++ b/lib/client/types/config.ts @@ -44,6 +44,7 @@ export interface ConnectionValidation { method?: string; auth: ConnectionHeaderAuth | ConnectionBasicAuth; body?: any; + headers?: Record; } export interface ConnectionHeaderAuth { diff --git a/lib/client/utils/connectionValidation.ts b/lib/client/utils/connectionValidation.ts index 686af90a6..a2f9fcf8e 100644 --- a/lib/client/utils/connectionValidation.ts +++ b/lib/client/utils/connectionValidation.ts @@ -10,7 +10,7 @@ export const validateConnection = async (config: ConnectionConfig) => { const validation = config.validations[i] ?? {}; const method = validation?.method ?? 'GET'; const { auth, url } = validation; - const headers: Record = {}; + const headers: Record = validation?.headers ?? {}; headers['user-agent'] = `Snyk Broker client ${version}`; switch (auth?.type) { case 'basic': diff --git a/lib/common/relay/prepareRequest.ts b/lib/common/relay/prepareRequest.ts index b6a9cc95f..f8012e48a 100644 --- a/lib/common/relay/prepareRequest.ts +++ b/lib/common/relay/prepareRequest.ts @@ -205,6 +205,22 @@ export const prepareRequestFromFilterResult = async ( logger.error({ error }, 'error while signing github commit'); } } + if ( + payload.headers && + payload.headers['x-broker-content-type'] === + 'application/x-www-form-urlencoded' + ) { + payload.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + if (payload.body) { + const jsonBody = JSON.parse(payload.body) as Record; + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(jsonBody)) { + params.append(key, value.toString()); + } + payload.body = params.toString(); + } + } + if (options.config && options.config.LOG_ENABLE_BODY === 'true') { logContext.requestBody = payload.body; } diff --git a/test/unit/relay-response-body-form-url-encoded.test.ts b/test/unit/relay-response-body-form-url-encoded.test.ts new file mode 100644 index 000000000..89afa4e37 --- /dev/null +++ b/test/unit/relay-response-body-form-url-encoded.test.ts @@ -0,0 +1,280 @@ +const PORT = 8001; +process.env.BROKER_SERVER_URL = `http://localhost:${PORT}`; + +jest.mock('../../lib/common/http/request'); +import { WebSocketConnection } from '../../lib/client/types/client'; +import { makeRequestToDownstream } from '../../lib/common/http/request'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const mockedFn = makeRequestToDownstream.mockImplementation((data) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return data; +}); + +import { forwardWebSocketRequest as relay } from '../../lib/common/relay/forwardWebsocketRequest'; +import { + LoadedClientOpts, + LoadedServerOpts, +} from '../../lib/common/types/options'; + +const dummyWebsocketHandler: WebSocketConnection = { + destroy: () => { + return; + }, + latency: 0, + options: { + ping: 0, + pong: 0, + queueSize: Infinity, + reconnect: '', + stategy: '', + timeout: 100, + transport: '', + }, + send: () => {}, + serverId: '0', + socket: {}, + supportedIntegrationType: 'github', + transport: '', + url: '', + on: () => {}, + readyState: 3, +}; + +const dummyLoadedFilters = { + private: () => { + return { url: '/', auth: '', stream: true }; + }, + public: () => { + return { url: '/', auth: '', stream: true }; + }, +}; + +describe('body relay', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + delete process.env.BROKER_SERVER_URL; + jest.clearAllMocks(); + }); + + it('relay swaps body values found in BROKER_VAR_SUB with application/x-www-form-urlencoded content-type', (done) => { + expect.hasAssertions(); + + const brokerToken = 'test-broker'; + + const config = { + HOST: 'localhost', + PORT: '8001', + }; + const options: LoadedClientOpts | LoadedServerOpts = { + filters: { + private: [ + { + method: 'any', + url: '/*', + }, + ], + public: [], + }, + config, + port: 8001, + loadedFilters: dummyLoadedFilters, + }; + + const route = relay(options, dummyWebsocketHandler)(brokerToken); + + const body = { + BROKER_VAR_SUB: ['url'], + url: '${HOST}:${PORT}/webhook', + }; + const headers = { + 'x-broker-content-type': 'application/x-www-form-urlencoded', + }; + + route( + { + url: '/', + method: 'POST', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + body: Buffer.from(JSON.stringify(body)), + headers, + }, + () => { + expect(makeRequestToDownstream).toHaveBeenCalledTimes(1); + const arg = mockedFn.mock.calls[0][0]; + expect(arg.body).toEqual( + `BROKER_VAR_SUB=url&url=${config.HOST}%3A${config.PORT}%2Fwebhook`, + ); + + done(); + }, + ); + }); + + it('relay swaps body values found in BROKER_VAR_SUB with JSON type', (done) => { + expect.hasAssertions(); + + const brokerToken = 'test-broker'; + + const config = { + HOST: 'localhost', + PORT: '8001', + }; + const options: LoadedClientOpts | LoadedServerOpts = { + filters: { + private: [ + { + method: 'any', + url: '/*', + }, + ], + public: [], + }, + config, + port: 8001, + loadedFilters: dummyLoadedFilters, + }; + + const route = relay(options, dummyWebsocketHandler)(brokerToken); + + const body = { + BROKER_VAR_SUB: ['url'], + url: '${HOST}:${PORT}/webhook', + }; + + route( + { + url: '/', + method: 'POST', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + body: Buffer.from(JSON.stringify(body)), + headers: {}, + }, + () => { + expect(makeRequestToDownstream).toHaveBeenCalledTimes(1); + const arg = mockedFn.mock.calls[0][0]; + expect(JSON.parse(arg.body).url).toEqual( + `${config.HOST}:${config.PORT}/webhook`, + ); + + done(); + }, + ); + }); + + it('relay does NOT swap body values found in BROKER_VAR_SUB if disabled with json type', (done) => { + expect.hasAssertions(); + + const brokerToken = 'test-broker'; + + const config = { + HOST: 'localhost', + PORT: '8001', + disableBodyVarsSubstitution: true, + brokerServerUrl: 'http://localhost:8001', + }; + + const options: LoadedClientOpts | LoadedServerOpts = { + filters: { + private: [ + { + method: 'any', + url: '/*', + }, + ], + public: [], + }, + config, + port: 8001, + loadedFilters: dummyLoadedFilters, + }; + const route = relay(options, dummyWebsocketHandler)(brokerToken); + + const body = { + BROKER_VAR_SUB: ['url'], + url: '${HOST}:${PORT}/webhook', + }; + + route( + { + url: '/', + method: 'POST', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + body: Buffer.from(JSON.stringify(body)), + headers: {}, + }, + () => { + expect(makeRequestToDownstream).toHaveBeenCalledTimes(1); + const arg = mockedFn.mock.calls[0][0]; + expect(JSON.parse(arg.body).url).toEqual('${HOST}:${PORT}/webhook'); + + done(); + }, + ); + }); + + it('relay does NOT swap body values found in BROKER_VAR_SUB if disabled with application/x-www-form-urlencoded content-type', (done) => { + expect.hasAssertions(); + + const brokerToken = 'test-broker'; + + const config = { + HOST: 'localhost', + PORT: '8001', + disableBodyVarsSubstitution: true, + }; + const options: LoadedClientOpts | LoadedServerOpts = { + filters: { + private: [ + { + method: 'any', + url: '/*', + }, + ], + public: [], + }, + config, + port: 8001, + loadedFilters: dummyLoadedFilters, + }; + + const route = relay(options, dummyWebsocketHandler)(brokerToken); + + const body = { + BROKER_VAR_SUB: ['url'], + url: '${HOST}:${PORT}/webhook', + }; + const headers = { + 'x-broker-content-type': 'application/x-www-form-urlencoded', + }; + + route( + { + url: '/', + method: 'POST', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + body: Buffer.from(JSON.stringify(body)), + headers, + }, + () => { + expect(makeRequestToDownstream).toHaveBeenCalledTimes(1); + const arg = mockedFn.mock.calls[0][0]; + expect(arg.body).toEqual( + 'BROKER_VAR_SUB=url&url=%24%7BHOST%7D%3A%24%7BPORT%7D%2Fwebhook', + ); + + done(); + }, + ); + }); +}); diff --git a/test/unit/relay-response-body-universal-form-url-encoded.test.ts b/test/unit/relay-response-body-universal-form-url-encoded.test.ts new file mode 100644 index 000000000..703cd7e9e --- /dev/null +++ b/test/unit/relay-response-body-universal-form-url-encoded.test.ts @@ -0,0 +1,188 @@ +const PORT = 8001; +process.env.BROKER_SERVER_URL = `http://localhost:${PORT}`; + +jest.mock('../../lib/common/http/request'); +import { WebSocketConnection } from '../../lib/client/types/client'; +import { makeRequestToDownstream } from '../../lib/common/http/request'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const mockedFn = makeRequestToDownstream.mockImplementation((data) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return data; +}); + +import { forwardWebSocketRequest as relay } from '../../lib/common/relay/forwardWebsocketRequest'; +import { + LoadedClientOpts, + LoadedServerOpts, +} from '../../lib/common/types/options'; + +const dummyWebsocketHandler: WebSocketConnection = { + destroy: () => { + return; + }, + latency: 0, + options: { + ping: 0, + pong: 0, + queueSize: Infinity, + reconnect: '', + stategy: '', + timeout: 100, + transport: '', + }, + send: () => {}, + serverId: '0', + socket: {}, + supportedIntegrationType: 'github', + transport: '', + url: '', + on: () => {}, + readyState: 3, +}; + +const dummyLoadedFilters = { + private: () => { + return { url: '/', auth: '', stream: true }; + }, + public: () => { + return { url: '/', auth: '', stream: true }; + }, +}; + +describe('body relay', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + delete process.env.BROKER_SERVER_URL; + jest.clearAllMocks(); + }); + + it('relay swaps body values found in BROKER_VAR_SUB application/x-www-form-urlencoded type', (done) => { + expect.hasAssertions(); + + const brokerToken = 'test-broker'; + + const config = { + universalBrokerEnabled: true, + connections: { + myconn: { + identifier: brokerToken, + HOST: 'localhost', + PORT: '8001', + }, + }, + }; + const options: LoadedClientOpts | LoadedServerOpts = { + filters: { + private: [ + { + method: 'any', + url: '/*', + }, + ], + public: [], + }, + config, + port: 8001, + loadedFilters: dummyLoadedFilters, + }; + + const route = relay(options, dummyWebsocketHandler)(brokerToken); + + const body = { + BROKER_VAR_SUB: ['url'], + url: '${HOST}:${PORT}/webhook', + }; + const headers = { + 'x-broker-content-type': 'application/x-www-form-urlencoded', + }; + + route( + { + url: '/', + method: 'POST', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + body: Buffer.from(JSON.stringify(body)), + headers: headers, + }, + () => { + expect(makeRequestToDownstream).toHaveBeenCalledTimes(1); + const arg = mockedFn.mock.calls[0][0]; + + expect(arg.headers['Content-Type']).toEqual( + 'application/x-www-form-urlencoded', + ); + expect(arg.body).toEqual( + `BROKER_VAR_SUB=url&url=${config.connections.myconn.HOST}%3A${config.connections.myconn.PORT}%2Fwebhook`, + ); + + done(); + }, + ); + }); + + it('relay does NOT swap body values found in BROKER_VAR_SUB if disabled', (done) => { + expect.hasAssertions(); + + const brokerToken = 'test-broker'; + + const config = { + universalBrokerEnabled: true, + connections: { + myconn: { + identifier: brokerToken, + HOST: 'localhost', + PORT: '8001', + }, + }, + disableBodyVarsSubstitution: true, + brokerServerUrl: 'http://localhost:8001', + }; + + const options: LoadedClientOpts | LoadedServerOpts = { + filters: { + private: [ + { + method: 'any', + url: '/*', + }, + ], + public: [], + }, + config, + port: 8001, + loadedFilters: dummyLoadedFilters, + }; + const route = relay(options, dummyWebsocketHandler)(brokerToken); + + const body = { + BROKER_VAR_SUB: ['url'], + url: '${HOST}:${PORT}/webhook', + }; + + route( + { + url: '/', + method: 'POST', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + body: Buffer.from(JSON.stringify(body)), + headers: {}, + }, + () => { + expect(makeRequestToDownstream).toHaveBeenCalledTimes(1); + const arg = mockedFn.mock.calls[0][0]; + expect(JSON.parse(arg.body).url).toEqual('${HOST}:${PORT}/webhook'); + + done(); + }, + ); + }); +}); diff --git a/test/unit/relay-response-headers-form-url-headers.test.ts b/test/unit/relay-response-headers-form-url-headers.test.ts new file mode 100644 index 000000000..ab09d5d92 --- /dev/null +++ b/test/unit/relay-response-headers-form-url-headers.test.ts @@ -0,0 +1,271 @@ +const PORT = 8001; +process.env.BROKER_SERVER_URL = `http://localhost:${PORT}`; +jest.mock('../../lib/common/http/request'); +import { WebSocketConnection } from '../../lib/client/types/client'; +import { makeRequestToDownstream } from '../../lib/common/http/request'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const mockedFn = makeRequestToDownstream.mockImplementation((data) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return data; +}); + +import { forwardWebSocketRequest as relay } from '../../lib/common/relay/forwardWebsocketRequest'; +import { + LoadedClientOpts, + LoadedServerOpts, +} from '../../lib/common/types/options'; + +const dummyWebsocketHandler: WebSocketConnection = { + destroy: () => { + return; + }, + latency: 0, + options: { + ping: 0, + pong: 0, + queueSize: Infinity, + reconnect: '', + stategy: '', + timeout: 100, + transport: '', + }, + send: () => {}, + serverId: '0', + socket: {}, + supportedIntegrationType: 'github', + transport: '', + url: '', + on: () => {}, + readyState: 3, +}; + +const dummyLoadedFilters = { + private: () => { + return { url: '/', auth: '', stream: true }; + }, + public: () => { + return { url: '/', auth: '', stream: true }; + }, +}; + +describe('header relay', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterAll(() => { + delete process.env.BROKER_SERVER_URL; + jest.clearAllMocks(); + }); + it('swaps header values found in BROKER_VAR_SUB with application/x-www-form-urlencoded content-type', (done) => { + expect.hasAssertions(); + + const brokerToken = 'test-broker'; + + const config = { + SECRET_TOKEN: 'very-secret', + VALUE: 'some-special-value', + }; + const options: LoadedClientOpts | LoadedServerOpts = { + filters: { + private: [ + { + method: 'any', + url: '/*', + }, + ], + public: [], + }, + config, + port: 8001, + loadedFilters: dummyLoadedFilters, + }; + const route = relay(options, dummyWebsocketHandler)(brokerToken); + + const headers = { + 'x-broker-content-type': 'application/x-www-form-urlencoded', + 'x-broker-var-sub': 'private-token,replaceme', + donttouch: 'not to be changed ${VALUE}', + 'private-token': 'Bearer ${SECRET_TOKEN}', + replaceme: 'replace ${VALUE}', + }; + + route( + { + url: '/', + method: 'GET', + headers: headers, + }, + () => { + expect(makeRequestToDownstream).toHaveBeenCalledTimes(1); + const arg = mockedFn.mock.calls[0][0]; + expect(arg.headers['Content-Type']).toEqual( + 'application/x-www-form-urlencoded', + ); + expect(arg.headers['private-token']).toEqual( + `Bearer ${config.SECRET_TOKEN}`, + ); + expect(arg.headers.replaceme).toEqual(`replace ${config.VALUE}`); + expect(arg.headers.donttouch).toEqual('not to be changed ${VALUE}'); + done(); + }, + ); + }); + + it('swaps header values found in BROKER_VAR_SUB with json type', (done) => { + expect.hasAssertions(); + + const brokerToken = 'test-broker'; + + const config = { + SECRET_TOKEN: 'very-secret', + VALUE: 'some-special-value', + }; + const options: LoadedClientOpts | LoadedServerOpts = { + filters: { + private: [ + { + method: 'any', + url: '/*', + }, + ], + public: [], + }, + config, + port: 8001, + loadedFilters: dummyLoadedFilters, + }; + const route = relay(options, dummyWebsocketHandler)(brokerToken); + + const headers = { + 'x-broker-var-sub': 'private-token,replaceme', + donttouch: 'not to be changed ${VALUE}', + 'private-token': 'Bearer ${SECRET_TOKEN}', + replaceme: 'replace ${VALUE}', + }; + + route( + { + url: '/', + method: 'GET', + headers: headers, + }, + () => { + expect(makeRequestToDownstream).toHaveBeenCalledTimes(1); + const arg = mockedFn.mock.calls[0][0]; + expect(arg.headers['private-token']).toEqual( + `Bearer ${config.SECRET_TOKEN}`, + ); + expect(arg.headers.replaceme).toEqual(`replace ${config.VALUE}`); + expect(arg.headers.donttouch).toEqual('not to be changed ${VALUE}'); + done(); + }, + ); + }); + + it('does NOT swap header values found in BROKER_VAR_SUB with json type', (done) => { + expect.hasAssertions(); + + const brokerToken = 'test-broker'; + + const config = { + SECRET_TOKEN: 'very-secret', + VALUE: 'some-special-value', + disableHeaderVarsSubstitution: true, + brokerServerUrl: 'http://localhost:8001', + }; + + const options: LoadedClientOpts | LoadedServerOpts = { + filters: { + private: [ + { + method: 'any', + url: '/*', + }, + ], + public: [], + }, + config, + port: 8001, + loadedFilters: dummyLoadedFilters, + }; + const route = relay(options, dummyWebsocketHandler)(brokerToken); + + const headers = { + 'x-broker-var-sub': 'private-token,replaceme', + donttouch: 'not to be changed ${VALUE}', + 'private-token': 'Bearer ${SECRET_TOKEN}', + replaceme: 'replace ${VALUE}', + }; + + route( + { + url: '/', + method: 'GET', + headers: headers, + }, + () => { + expect(makeRequestToDownstream).toHaveBeenCalledTimes(1); + const arg = mockedFn.mock.calls[0][0]; + expect(arg.headers['private-token']).toEqual('Bearer ${SECRET_TOKEN}'); + expect(arg.headers.replaceme).toEqual('replace ${VALUE}'); + expect(arg.headers.donttouch).toEqual('not to be changed ${VALUE}'); + done(); + }, + ); + }); + + it('does NOT swap header values found in BROKER_VAR_SUB with application/x-www-form-urlencoded content-type', (done) => { + expect.hasAssertions(); + + const brokerToken = 'test-broker'; + + const config = { + SECRET_TOKEN: 'very-secret', + VALUE: 'some-special-value', + disableHeaderVarsSubstitution: true, + }; + const options: LoadedClientOpts | LoadedServerOpts = { + filters: { + private: [ + { + method: 'any', + url: '/*', + }, + ], + public: [], + }, + config, + port: 8001, + loadedFilters: dummyLoadedFilters, + }; + const route = relay(options, dummyWebsocketHandler)(brokerToken); + + const headers = { + 'x-broker-content-type': 'application/x-www-form-urlencoded', + 'x-broker-var-sub': 'private-token,replaceme', + donttouch: 'not to be changed ${VALUE}', + 'private-token': 'Bearer ${SECRET_TOKEN}', + replaceme: 'replace ${VALUE}', + }; + + route( + { + url: '/', + method: 'GET', + headers: headers, + }, + () => { + expect(makeRequestToDownstream).toHaveBeenCalledTimes(1); + const arg = mockedFn.mock.calls[0][0]; + expect(arg.headers['private-token']).toEqual('Bearer ${SECRET_TOKEN}'); + expect(arg.headers.replaceme).toEqual('replace ${VALUE}'); + expect(arg.headers.donttouch).toEqual('not to be changed ${VALUE}'); + done(); + }, + ); + }); +}); diff --git a/test/unit/relay-response-headers-universal-form-url-headers.test.ts b/test/unit/relay-response-headers-universal-form-url-headers.test.ts new file mode 100644 index 000000000..7c5aa52a4 --- /dev/null +++ b/test/unit/relay-response-headers-universal-form-url-headers.test.ts @@ -0,0 +1,187 @@ +const PORT = 8001; +process.env.BROKER_SERVER_URL = `http://localhost:${PORT}`; +jest.mock('../../lib/common/http/request'); +import { WebSocketConnection } from '../../lib/client/types/client'; +import { makeRequestToDownstream } from '../../lib/common/http/request'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const mockedFn = makeRequestToDownstream.mockImplementation((data) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return data; +}); + +import { forwardWebSocketRequest as relay } from '../../lib/common/relay/forwardWebsocketRequest'; +import { + LoadedClientOpts, + LoadedServerOpts, +} from '../../lib/common/types/options'; + +const dummyWebsocketHandler: WebSocketConnection = { + destroy: () => { + return; + }, + latency: 0, + options: { + ping: 0, + pong: 0, + queueSize: Infinity, + reconnect: '', + stategy: '', + timeout: 100, + transport: '', + }, + send: () => {}, + serverId: '0', + socket: {}, + supportedIntegrationType: 'github', + transport: '', + url: '', + on: () => {}, + readyState: 3, +}; + +const dummyLoadedFilters = { + private: () => { + return { url: '/', auth: '', stream: true }; + }, + public: () => { + return { url: '/', auth: '', stream: true }; + }, +}; + +describe('header relay', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterAll(() => { + delete process.env.BROKER_SERVER_URL; + jest.clearAllMocks(); + }); + it('swaps header values found in BROKER_VAR_SUB', (done) => { + expect.hasAssertions(); + + const brokerToken = 'test-broker'; + + const config = { + universalBrokerEnabled: true, + connections: { + myconn: { + identifier: brokerToken, + SECRET_TOKEN: 'very-secret', + VALUE: 'some-special-value', + }, + }, + }; + const options: LoadedClientOpts | LoadedServerOpts = { + filters: { + private: [ + { + method: 'any', + url: '/*', + }, + ], + public: [], + }, + config, + port: 8001, + loadedFilters: dummyLoadedFilters, + }; + const route = relay(options, dummyWebsocketHandler)(brokerToken); + + const headers = { + 'x-broker-content-type': 'application/x-www-form-urlencoded', + 'x-broker-var-sub': 'private-token,replaceme', + donttouch: 'not to be changed ${VALUE}', + 'private-token': 'Bearer ${SECRET_TOKEN}', + replaceme: 'replace ${VALUE}', + }; + + route( + { + url: '/', + method: 'GET', + headers: headers, + }, + () => { + expect(makeRequestToDownstream).toHaveBeenCalledTimes(1); + const arg = mockedFn.mock.calls[0][0]; + expect(arg.headers['Content-Type']).toEqual( + 'application/x-www-form-urlencoded', + ); + expect(arg.headers['private-token']).toEqual( + `Bearer ${config.connections.myconn.SECRET_TOKEN}`, + ); + expect(arg.headers.replaceme).toEqual( + `replace ${config.connections.myconn.VALUE}`, + ); + expect(arg.headers.donttouch).toEqual('not to be changed ${VALUE}'); + done(); + }, + ); + }); + + it('does NOT swap header values found in BROKER_VAR_SUB', (done) => { + expect.hasAssertions(); + + const brokerToken = 'test-broker'; + + const config = { + universalBrokerEnabled: true, + connections: { + myconn: { + identifier: brokerToken, + SECRET_TOKEN: 'very-secret', + VALUE: 'some-special-value', + }, + }, + disableHeaderVarsSubstitution: true, + brokerServerUrl: 'http://localhost:8001', + }; + + const options: LoadedClientOpts | LoadedServerOpts = { + filters: { + private: [ + { + method: 'any', + url: '/*', + }, + ], + public: [], + }, + config, + port: 8001, + loadedFilters: dummyLoadedFilters, + }; + const route = relay(options, dummyWebsocketHandler)(brokerToken); + + const headers = { + 'x-broker-content-type': 'application/x-www-form-urlencoded', + 'x-broker-var-sub': 'private-token,replaceme', + donttouch: 'not to be changed ${VALUE}', + 'private-token': 'Bearer ${SECRET_TOKEN}', + replaceme: 'replace ${VALUE}', + }; + + route( + { + url: '/', + method: 'GET', + headers: headers, + }, + () => { + expect(makeRequestToDownstream).toHaveBeenCalledTimes(1); + const arg = mockedFn.mock.calls[0][0]; + expect(arg.headers['Content-Type']).toEqual( + 'application/x-www-form-urlencoded', + ); + expect(arg.headers['private-token']).toEqual('Bearer ${SECRET_TOKEN}'); + expect(arg.headers.replaceme).toEqual('replace ${VALUE}'); + expect(arg.headers.donttouch).toEqual('not to be changed ${VALUE}'); + done(); + }, + ); + }); +});