diff --git a/.gitignore b/.gitignore index 7fab6e0..4b8c993 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ http_requests json_responses integration/**/package-lock.json unit-tests-result.json -.gradle \ No newline at end of file +.gradle +build +target \ No newline at end of file diff --git a/README.md b/README.md index 0039ac6..f4e2305 100644 --- a/README.md +++ b/README.md @@ -307,8 +307,9 @@ let options = { 'EXHORT_PIP3_PATH' : '/path/to/pip3', 'EXHORT_PYTHON_PATH' : '/path/to/python', 'EXHORT_PIP_PATH' : '/path/to/pip', - 'EXHORT_GRADLE_PATH' : '/path/to/gradle' - + 'EXHORT_GRADLE_PATH' : '/path/to/gradle', + // Configure proxy for all requests + 'EXHORT_PROXY_URL': 'http://proxy.example.com:8080' } // Get stack analysis in JSON format ( all package managers, pom.xml is as an example here) @@ -322,6 +323,27 @@ let componentAnalysis = await exhort.componentAnalysis('/path/to/pom.xml', optio **_Environment variables takes precedence._**

+

Proxy Configuration

+

+You can configure a proxy for all HTTP/HTTPS requests made by the API. This is useful when your environment requires going through a proxy to access external services. + +You can set the proxy URL in two ways: + +1. Using environment variable: +```shell +export EXHORT_PROXY_URL=http://proxy.example.com:8080 +``` + +2. Using the options object when calling the API programmatically: +```javascript +const options = { + 'EXHORT_PROXY_URL': 'http://proxy.example.com:8080' +} +``` + +The proxy URL should be in the format: `http://host:port` or `https://host:port`. The API will automatically use the appropriate protocol (HTTP or HTTPS) based on the proxy URL provided. +

+

Customizing Executables

This project uses each ecosystem's executable for creating dependency trees. These executables are expected to be diff --git a/src/analysis.js b/src/analysis.js index d10c030..260e203 100644 --- a/src/analysis.js +++ b/src/analysis.js @@ -2,6 +2,8 @@ import fs from "node:fs"; import path from "node:path"; import {EOL} from "os"; import {RegexNotToBeLogged, getCustom} from "./tools.js"; +import http from 'node:http'; +import https from 'node:https'; export default { requestComponent, requestStack, validateToken } @@ -9,6 +11,23 @@ const rhdaTokenHeader = "rhda-token"; const rhdaSourceHeader = "rhda-source" const rhdaOperationTypeHeader = "rhda-operation-type" +/** + * Adds proxy agent configuration to fetch options if a proxy URL is specified + * @param {Object} options - The base fetch options + * @param {Object} opts - The exhort options that may contain proxy configuration + * @returns {Object} The fetch options with proxy agent if applicable + */ +function addProxyAgent(options, opts) { + const proxyUrl = getCustom('EXHORT_PROXY_URL', null, opts); + if (proxyUrl) { + const proxyUrlObj = new URL(proxyUrl); + options.agent = proxyUrlObj.protocol === 'https:' + ? new https.Agent({ proxy: proxyUrl }) + : new http.Agent({ proxy: proxyUrl }); + } + return options; +} + /** * Send a stack analysis request and get the report as 'text/html' or 'application/json'. * @param {import('./provider').Provider | import('./providers/base_java.js').default } provider - the provided data for constructing the request @@ -29,7 +48,8 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) { if (process.env["EXHORT_DEBUG"] === "true") { console.log("Starting time of sending stack analysis request to exhort server= " + startTime) } - let resp = await fetch(`${url}/api/v4/analysis`, { + + const fetchOptions = addProxyAgent({ method: 'POST', headers: { 'Accept': html ? 'text/html' : 'application/json', @@ -37,7 +57,9 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) { ...getTokenHeaders(opts) }, body: provided.content - }) + }, opts); + + let resp = await fetch(`${url}/api/v4/analysis`, fetchOptions) let result if(resp.status === 200) { if (!html) { @@ -82,7 +104,8 @@ async function requestComponent(provider, manifest, url, opts = {}) { if (process.env["EXHORT_DEBUG"] === "true") { console.log("Starting time of sending component analysis request to exhort server= " + new Date()) } - let resp = await fetch(`${url}/api/v4/analysis`, { + + const fetchOptions = addProxyAgent({ method: 'POST', headers: { 'Accept': 'application/json', @@ -90,7 +113,9 @@ async function requestComponent(provider, manifest, url, opts = {}) { ...getTokenHeaders(opts), }, body: provided.content - }) + }, opts); + + let resp = await fetch(`${url}/api/v4/analysis`, fetchOptions) let result if(resp.status === 200) { result = await resp.json() @@ -119,13 +144,14 @@ async function requestComponent(provider, manifest, url, opts = {}) { * @return {Promise} return the HTTP status Code of the response from the validate token request. */ async function validateToken(url, opts = {}) { - let resp = await fetch(`${url}/api/v4/token`, { + const fetchOptions = addProxyAgent({ method: 'GET', headers: { - // 'Accept': 'text/plain', ...getTokenHeaders(opts), } - }) + }, opts); + + let resp = await fetch(`${url}/api/v4/token`, fetchOptions) if (process.env["EXHORT_DEBUG"] === "true") { let exRequestId = resp.headers.get("ex-request-id"); if (exRequestId) { diff --git a/test/analysis.test.js b/test/analysis.test.js index 7d47f25..a21e7c9 100644 --- a/test/analysis.test.js +++ b/test/analysis.test.js @@ -202,4 +202,72 @@ suite('testing the analysis module for sending api requests', () => { } )) }) + + suite('verify proxy configuration', () => { + let fakeManifest = 'fake-file.typ' + let stackProviderStub = sinon.stub() + stackProviderStub.withArgs(fakeManifest).returns(fakeProvided) + let fakeProvider = { + provideComponent: () => {}, + provideStack: stackProviderStub, + isSupported: () => {} + }; + + afterEach(() => { + delete process.env['EXHORT_PROXY_URL'] + }) + + test('when HTTP proxy is configured, verify agent is set correctly', interceptAndRun( + rest.post(`${backendUrl}/api/v3/analysis`, (req, res, ctx) => { + // The request should go through the proxy + return res(ctx.json({ok: 'ok'})) + }), + async () => { + const httpProxyUrl = 'http://proxy.example.com:8080' + const options = { + 'EXHORT_PROXY_URL': httpProxyUrl + } + let res = await analysis.requestStack(fakeProvider, fakeManifest, backendUrl, false, options) + expect(res).to.deep.equal({ok: 'ok'}) + } + )) + + test('when HTTPS proxy is configured, verify agent is set correctly', interceptAndRun( + rest.post(`${backendUrl}/api/v3/analysis`, (req, res, ctx) => { + // The request should go through the proxy + return res(ctx.json({ok: 'ok'})) + }), + async () => { + const httpsProxyUrl = 'https://proxy.example.com:8080' + const options = { + 'EXHORT_PROXY_URL': httpsProxyUrl + } + let res = await analysis.requestStack(fakeProvider, fakeManifest, backendUrl, false, options) + expect(res).to.deep.equal({ok: 'ok'}) + } + )) + + test('when proxy is configured via environment variable, verify agent is set correctly', interceptAndRun( + rest.post(`${backendUrl}/api/v3/analysis`, (req, res, ctx) => { + // The request should go through the proxy + return res(ctx.json({ok: 'ok'})) + }), + async () => { + process.env['EXHORT_PROXY_URL'] = 'http://proxy.example.com:8080' + let res = await analysis.requestStack(fakeProvider, fakeManifest, backendUrl) + expect(res).to.deep.equal({ok: 'ok'}) + } + )) + + test('when no proxy is configured, verify no agent is set', interceptAndRun( + rest.post(`${backendUrl}/api/v3/analysis`, (req, res, ctx) => { + // The request should go directly without proxy + return res(ctx.json({ok: 'ok'})) + }), + async () => { + let res = await analysis.requestStack(fakeProvider, fakeManifest, backendUrl) + expect(res).to.deep.equal({ok: 'ok'}) + } + )) + }) })