From 0994c51c9088cb2a52fe51649777be3bfa7141f7 Mon Sep 17 00:00:00 2001
From: Ruben Romero Montes
Date: Tue, 13 May 2025 10:29:21 +0200
Subject: [PATCH 1/2] feat: support configure a proxy url
Signed-off-by: Ruben Romero Montes
---
.gitignore | 4 +++-
README.md | 26 ++++++++++++++++++++++++--
src/analysis.js | 40 +++++++++++++++++++++++++++++++++-------
3 files changed, 60 insertions(+), 10 deletions(-)
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) {
From 38a4cad3e6f54524950d44537260403f7f132dae Mon Sep 17 00:00:00 2001
From: Ruben Romero Montes
Date: Tue, 13 May 2025 10:38:45 +0200
Subject: [PATCH 2/2] feat: add tests for proxy
Signed-off-by: Ruben Romero Montes
---
test/analysis.test.js | 68 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 68 insertions(+)
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'})
+ }
+ ))
+ })
})