diff --git a/package-lock.json b/package-lock.json index b6bb90c3c..a0e7d3348 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21276,6 +21276,8 @@ "@jupiterone/integration-sdk-core": "^17.0.2", "@lifeomic/attempt": "^3.0.3", "form-data": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "lodash": "^4.17.21", "node-fetch": "^2.7.0" }, @@ -21288,6 +21290,41 @@ "node": ">=18.0.0 <21.x" } }, + "packages/integration-sdk-http-client/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "packages/integration-sdk-http-client/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "packages/integration-sdk-http-client/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "packages/integration-sdk-http-client/node_modules/node-fetch": { "version": "2.7.0", "license": "MIT", diff --git a/packages/integration-sdk-http-client/package.json b/packages/integration-sdk-http-client/package.json index 522e7295c..5009617fe 100644 --- a/packages/integration-sdk-http-client/package.json +++ b/packages/integration-sdk-http-client/package.json @@ -27,6 +27,8 @@ "@jupiterone/integration-sdk-core": "^17.0.2", "@lifeomic/attempt": "^3.0.3", "form-data": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "lodash": "^4.17.21", "node-fetch": "^2.7.0" }, diff --git a/packages/integration-sdk-http-client/src/__tests__/index.test.ts b/packages/integration-sdk-http-client/src/__tests__/index.test.ts index 60997ca7b..b98b4f143 100644 --- a/packages/integration-sdk-http-client/src/__tests__/index.test.ts +++ b/packages/integration-sdk-http-client/src/__tests__/index.test.ts @@ -294,6 +294,39 @@ describe('APIClient', () => { expect(spy).not.toHaveBeenCalled(); }); + it('should use HTTP_PROXY environment variable if set', async () => { + process.env.HTTP_PROXY = 'http://proxy.example.com:8080'; + + authHeadersFn.mockReturnValue({ + Authorization: 'Bearer test-token', + }); + const mockResponse = {} as Response; + (fetch as unknown as jest.Mock).mockResolvedValue(mockResponse); + + const client = new MockAPIClient({ + baseUrl: 'https://api.example.com', + logger: mockLogger, + integrationConfig: {}, + }); + + await (client as any).request('/test', { + method: 'GET', + body: { test: 'test' }, + }); + + // Check that fetch was called with an agent that is a HttpsProxyAgent and has the correct proxy URL + const fetchOptions = (fetch as unknown as jest.Mock).mock.calls[0][1]; + const { HttpsProxyAgent } = require('https-proxy-agent'); + expect(fetchOptions.agent).toBeInstanceOf(HttpsProxyAgent); + expect( + fetchOptions.agent.proxy?.href || + fetchOptions.agent.options?.proxy?.href || + fetchOptions.agent.options?.url, // fallback for some versions + ).toContain('http://proxy.example.com:8080'); + + delete process.env.HTTP_PROXY; + }); + describe('fmtBody', () => { test('should format body as JSON string when bodyType is json', async () => { const mockResponse = {} as Response; @@ -564,6 +597,4 @@ describe('APIClient', () => { ); }); }); - - // Add more test cases for other methods as needed }); diff --git a/packages/integration-sdk-http-client/src/client.ts b/packages/integration-sdk-http-client/src/client.ts index dffee8bd4..b1a28e9f6 100644 --- a/packages/integration-sdk-http-client/src/client.ts +++ b/packages/integration-sdk-http-client/src/client.ts @@ -1,5 +1,6 @@ import { join as joinPath } from 'node:path/posix'; import fetch, { Headers, Response } from 'node-fetch'; +import { HttpsProxyAgent } from 'https-proxy-agent'; import { IntegrationError, IntegrationInstanceConfig, @@ -274,6 +275,22 @@ export abstract class BaseAPIClient { fmtBody = JSON.stringify(body); } } + + // Proxy agent logic + const proxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; + let finalAgent: Agent | undefined; + if (proxy) { + // If an agent is provided, wrap it with the proxy agent + finalAgent = new HttpsProxyAgent(proxy); + // If a custom agent is provided, set it as the 'secureProxy' option for HttpsProxyAgent + if (agent) { + // @ts-expect-error: HttpsProxyAgent accepts 'secureProxy' in its options + finalAgent.options = { ...finalAgent.options, secureProxy: agent }; + } + } else { + finalAgent = agent ?? this.internalGetDefaultAgent(); + } + const response = await fetch(url, { method, headers: { @@ -288,7 +305,7 @@ export abstract class BaseAPIClient { ...headers, }, body: fmtBody, - agent: agent ?? this.internalGetDefaultAgent(), + agent: finalAgent, }); return response; }