Skip to content

feat: support configure a proxy url #197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ http_requests
json_responses
integration/**/package-lock.json
unit-tests-result.json
.gradle
.gradle
build
target
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -322,6 +323,27 @@ let componentAnalysis = await exhort.componentAnalysis('/path/to/pom.xml', optio
**_Environment variables takes precedence._**
</p>

<h4>Proxy Configuration</h4>
<p>
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.
</p>

<h4>Customizing Executables</h4>
<p>
This project uses each ecosystem's executable for creating dependency trees. These executables are expected to be
Expand Down
40 changes: 33 additions & 7 deletions src/analysis.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import fs from "node:fs";
import path from "node:path";
import {EOL} from "os";

Check warning on line 3 in src/analysis.js

View workflow job for this annotation

GitHub Actions / Lint and test project (18)

Imports should be sorted alphabetically

Check warning on line 3 in src/analysis.js

View workflow job for this annotation

GitHub Actions / Lint and test project (latest)

Imports should be sorted alphabetically
import {RegexNotToBeLogged, getCustom} from "./tools.js";

Check warning on line 4 in src/analysis.js

View workflow job for this annotation

GitHub Actions / Lint and test project (18)

Expected 'multiple' syntax before 'single' syntax

Check warning on line 4 in src/analysis.js

View workflow job for this annotation

GitHub Actions / Lint and test project (latest)

Expected 'multiple' syntax before 'single' syntax
import http from 'node:http';
import https from 'node:https';

export default { requestComponent, requestStack, validateToken }

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
Expand All @@ -29,15 +48,18 @@
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',
'Content-Type': provided.contentType,
...getTokenHeaders(opts)
},
body: provided.content
})
}, opts);

let resp = await fetch(`${url}/api/v4/analysis`, fetchOptions)
let result
if(resp.status === 200) {
if (!html) {
Expand Down Expand Up @@ -82,15 +104,18 @@
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',
'Content-Type': provided.contentType,
...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()
Expand Down Expand Up @@ -119,13 +144,14 @@
* @return {Promise<number>} 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) {
Expand Down
68 changes: 68 additions & 0 deletions test/analysis.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'})
}
))
})
})
Loading