mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-29 01:39:24 +08:00
fix(core): Apply TLS options per hop when requests go through a proxy (#35518)
This commit is contained in:
@@ -68,6 +68,22 @@ describe('buildNodeAgents', () => {
|
||||
|
||||
expect(getAgentOptions(httpAgent).keepAlive).toBe(true);
|
||||
});
|
||||
|
||||
it('proxy: explicit URL → keeps the target TLS options off the proxy connection', () => {
|
||||
const agents = buildNodeAgents('https://proxy.internal:3128', 'disabled', {
|
||||
servername: 'target.example.com',
|
||||
ca: 'TARGET_CA',
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
for (const agent of [agents.httpAgent, agents.httpsAgent]) {
|
||||
const { connectOpts } = agent as unknown as { connectOpts: Record<string, unknown> };
|
||||
expect(connectOpts).toMatchObject({ host: 'proxy.internal', port: 3128 });
|
||||
expect(connectOpts).not.toHaveProperty('servername');
|
||||
expect(connectOpts).not.toHaveProperty('ca');
|
||||
expect(connectOpts).not.toHaveProperty('rejectUnauthorized');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('rejects a caller-provided lookup (managed by the SSRF policy)', () => {
|
||||
|
||||
@@ -165,6 +165,16 @@ describe('buildAxiosConfigFromLegacyRequest', () => {
|
||||
expect((axiosOptions.httpsAgent as HttpsAgent).options.servername).toEqual('example.de');
|
||||
});
|
||||
|
||||
// SNI takes a hostname, never an IP literal (RFC 6066 §3).
|
||||
test.each([
|
||||
['IPv4', 'https://185.90.154.8/foo'],
|
||||
['IPv6', 'https://[2001:db8::1]/foo'],
|
||||
])('should set no SNI when the host is an %s literal', async (_label, url) => {
|
||||
const axiosOptions = await buildAxiosConfigFromLegacyRequest({ url });
|
||||
|
||||
expect((axiosOptions.httpsAgent as HttpsAgent).options.servername).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('should set SSL certificates', () => {
|
||||
const agentOptions: SecureContextOptions = {
|
||||
ca: TEST_CA_CERT,
|
||||
|
||||
@@ -701,4 +701,14 @@ describe('buildAgentOptions', () => {
|
||||
expect(options.keepAlive).toBe(true);
|
||||
expect(options.servername).toBe('api.example.com');
|
||||
});
|
||||
|
||||
// SNI takes a hostname, never an IP literal (RFC 6066 §3).
|
||||
it.each([
|
||||
['IPv4', 'https://185.90.154.8/v1'],
|
||||
['IPv6', 'https://[2001:db8::1]/v1'],
|
||||
])('sets no servername when the host is an %s literal', (_label, url) => {
|
||||
const options = buildAgentOptions({ method: 'GET', url });
|
||||
|
||||
expect(options.servername).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
resolveLegacyRequestTarget,
|
||||
searchForHeader,
|
||||
setAxiosAgents,
|
||||
sniFor,
|
||||
} from './utils';
|
||||
import type { SsrfBridge } from '../../ssrf';
|
||||
|
||||
@@ -32,10 +33,10 @@ import type { SsrfBridge } from '../../ssrf';
|
||||
* @deprecated Backs the deprecated `request` helpers.
|
||||
*/
|
||||
export function buildLegacyAgentOptions(requestObject: IRequestOptions): AgentOptions {
|
||||
const host = getHostFromRequestObject(requestObject);
|
||||
const servername = sniFor(getHostFromRequestObject(requestObject));
|
||||
const agentOptions: AgentOptions = { ...requestObject.agentOptions };
|
||||
if (host) {
|
||||
agentOptions.servername = host;
|
||||
if (servername) {
|
||||
agentOptions.servername = servername;
|
||||
}
|
||||
if (requestObject.rejectUnauthorized === false) {
|
||||
agentOptions.rejectUnauthorized = false;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
isProxyPotentiallyActive,
|
||||
isRedirectStatus,
|
||||
resolveProxyOption,
|
||||
sniFor,
|
||||
throwIfDomainNotAllowed,
|
||||
tryParseUrl,
|
||||
validateUrlSsrf,
|
||||
@@ -120,12 +121,11 @@ function buildRedirectHopAgents(
|
||||
nextUrl: string,
|
||||
policy: SsrfRedirectPolicy,
|
||||
): Pick<AxiosRequestConfig, 'httpAgent' | 'httpsAgent'> {
|
||||
const host = tryParseUrl(nextUrl)?.hostname;
|
||||
const customProxyUrl = policy.proxyConfig ? getUrlFromProxyConfig(policy.proxyConfig) : null;
|
||||
const proxy = resolveProxyOption(customProxyUrl);
|
||||
return buildNodeAgents(proxy, policy.ssrf, {
|
||||
...policy.agentOptions,
|
||||
...(host ? { servername: host } : {}),
|
||||
servername: sniFor(tryParseUrl(nextUrl)?.hostname),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type IRequestOptions,
|
||||
type IgnoreStatusErrorConfig,
|
||||
} from 'n8n-workflow';
|
||||
import net from 'node:net';
|
||||
|
||||
import { hasProxyEnvironmentVariables } from '../../proxy/proxy-resolution';
|
||||
import type { SsrfBridge } from '../../ssrf';
|
||||
@@ -57,6 +58,24 @@ export function searchForHeader(config: AxiosRequestConfig, headerName: string)
|
||||
return headerNames.find((thisHeader) => thisHeader.toLowerCase() === headerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* The SNI to announce when connecting to `host`.
|
||||
*
|
||||
* @returns the host, or `undefined` when it carries no name to announce: SNI takes a
|
||||
* hostname and never an IP literal (RFC 6066 §3), and Node ignores IP values.
|
||||
*/
|
||||
export function sniFor(host: string | null | undefined): string | undefined {
|
||||
if (!host) {
|
||||
return undefined;
|
||||
}
|
||||
// A URL hostname keeps the brackets of an IPv6 literal; `net.isIP` expects it bare.
|
||||
const unbracketed = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
||||
if (net.isIP(unbracketed)) {
|
||||
return undefined;
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
/** Extracts the hostname from a request object's URL or URI. */
|
||||
export const getHostFromRequestObject = (
|
||||
requestObject: Partial<{
|
||||
@@ -115,7 +134,7 @@ export const getBeforeRedirectFn =
|
||||
|
||||
const redirectAgentOptions: AgentOptions = {
|
||||
...agentOptions,
|
||||
servername: redirectedRequest.hostname,
|
||||
servername: sniFor(redirectedRequest.hostname as string | undefined),
|
||||
};
|
||||
const customProxyUrl = proxyConfig ? getUrlFromProxyConfig(proxyConfig) : null;
|
||||
const proxy = resolveProxyOption(customProxyUrl);
|
||||
@@ -248,10 +267,10 @@ export function isFormDataInstance(data: unknown): data is FormData {
|
||||
* Shared by the axios config builder and the manual redirect follower so both derive agents the same way.
|
||||
*/
|
||||
export function buildAgentOptions(n8nRequest: IHttpRequestOptions): AgentOptions {
|
||||
const host = getHostFromRequestObject(n8nRequest);
|
||||
const servername = sniFor(getHostFromRequestObject(n8nRequest));
|
||||
const agentOptions: AgentOptions = { ...n8nRequest.agentOptions };
|
||||
if (host) {
|
||||
agentOptions.servername = host;
|
||||
if (servername) {
|
||||
agentOptions.servername = servername;
|
||||
}
|
||||
if (n8nRequest.skipSslCertificateValidation === true) {
|
||||
agentOptions.rejectUnauthorized = false;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import type { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import http from 'node:http';
|
||||
import type { LookupFunction } from 'node:net';
|
||||
|
||||
import { EnvProxyRouter } from './env-proxy-router';
|
||||
import type { NodeAgentOptions } from './node-agents';
|
||||
import { createProxiedHttpAgent } from '../proxy/proxied-agents';
|
||||
|
||||
type HttpAddRequestArgs = Parameters<HttpProxyAgent<string>['addRequest']>;
|
||||
type HttpProxyClientReq = HttpAddRequestArgs[0];
|
||||
@@ -22,10 +23,8 @@ export class EnvProxyHttpAgent extends http.Agent {
|
||||
|
||||
constructor(lookup?: LookupFunction, agentOptions?: NodeAgentOptions) {
|
||||
super({ ...agentOptions, lookup });
|
||||
this.router = new EnvProxyRouter(
|
||||
'http',
|
||||
80,
|
||||
(proxyUrl) => new HttpProxyAgent(proxyUrl, { ...agentOptions }),
|
||||
this.router = new EnvProxyRouter('http', 80, (proxyUrl) =>
|
||||
createProxiedHttpAgent(proxyUrl, agentOptions),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import type { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import type http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import type { LookupFunction } from 'node:net';
|
||||
|
||||
import { EnvProxyRouter } from './env-proxy-router';
|
||||
import type { NodeAgentOptions } from './node-agents';
|
||||
import { createProxiedHttpsAgent } from '../proxy/proxied-agents';
|
||||
|
||||
type HttpsProxyReqOpts = Parameters<HttpsProxyAgent<string>['addRequest']>[1];
|
||||
|
||||
@@ -16,10 +17,8 @@ export class EnvProxyHttpsAgent extends https.Agent {
|
||||
|
||||
constructor(lookup?: LookupFunction, agentOptions?: NodeAgentOptions) {
|
||||
super({ ...agentOptions, lookup });
|
||||
this.router = new EnvProxyRouter(
|
||||
'https',
|
||||
443,
|
||||
(proxyUrl) => new HttpsProxyAgent(proxyUrl, { ...agentOptions }),
|
||||
this.router = new EnvProxyRouter('https', 443, (proxyUrl) =>
|
||||
createProxiedHttpsAgent(proxyUrl, agentOptions),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
@@ -7,6 +5,7 @@ import type { LookupFunction } from 'node:net';
|
||||
|
||||
import { EnvProxyHttpAgent } from './env-proxy-http-agent';
|
||||
import { EnvProxyHttpsAgent } from './env-proxy-https-agent';
|
||||
import { createProxiedHttpAgent, createProxiedHttpsAgent } from '../proxy/proxied-agents';
|
||||
import type { SsrfBridge } from '../ssrf';
|
||||
|
||||
/**
|
||||
@@ -63,6 +62,10 @@ export type NodeAgentOptions = https.AgentOptions;
|
||||
* always overrides anything in `agentOptions`. Passing `agentOptions.lookup`
|
||||
* therefore has no effect and is rejected to avoid a false sense of control over
|
||||
* DNS resolution.
|
||||
*
|
||||
* `agentOptions` describes the connection to the **target**. Behind a proxy its TLS
|
||||
* options travel with the tunnelled session rather than the proxy handshake
|
||||
* (see `proxy/proxied-agents.ts`).
|
||||
*/
|
||||
export function buildNodeAgents(
|
||||
proxy: ProxyOption,
|
||||
@@ -100,8 +103,8 @@ export function buildNodeAgents(
|
||||
|
||||
// Explicit proxy URL. No direct path, so no SSRF lookup is injected.
|
||||
return {
|
||||
httpAgent: new HttpProxyAgent(proxy as string, { ...agentOptions }),
|
||||
httpsAgent: new HttpsProxyAgent(proxy as string, { ...agentOptions }),
|
||||
httpAgent: createProxiedHttpAgent(proxy, agentOptions),
|
||||
httpsAgent: createProxiedHttpsAgent(proxy, agentOptions),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import type http from 'node:http';
|
||||
import type https from 'node:https';
|
||||
import type net from 'node:net';
|
||||
import type { MockInstance } from 'vitest';
|
||||
|
||||
import { createProxiedHttpAgent, createProxiedHttpsAgent } from '../proxied-agents';
|
||||
|
||||
const PROXY_URL = 'https://proxy.internal:3128';
|
||||
|
||||
const TARGET_TLS: https.AgentOptions = {
|
||||
servername: 'target.example.com',
|
||||
ca: 'TARGET_CA',
|
||||
cert: 'TARGET_CERT',
|
||||
key: 'TARGET_KEY',
|
||||
passphrase: 'TARGET_PASSPHRASE',
|
||||
};
|
||||
|
||||
type ConnectOpts = Parameters<HttpsProxyAgent<string>['connect']>[1];
|
||||
|
||||
const targetRequestOpts = {
|
||||
host: 'target.example.com',
|
||||
port: 443,
|
||||
secureEndpoint: true,
|
||||
} as ConnectOpts;
|
||||
|
||||
describe('createProxiedHttpsAgent', () => {
|
||||
it('keeps the target SNI and trust material off the connection to the proxy', () => {
|
||||
const agent = createProxiedHttpsAgent(PROXY_URL, TARGET_TLS);
|
||||
|
||||
expect(agent.connectOpts).toMatchObject({ host: 'proxy.internal', port: 3128 });
|
||||
expect(agent.connectOpts).not.toHaveProperty('servername');
|
||||
expect(agent.connectOpts).not.toHaveProperty('ca');
|
||||
expect(agent.connectOpts).not.toHaveProperty('cert');
|
||||
expect(agent.connectOpts).not.toHaveProperty('key');
|
||||
expect(agent.connectOpts).not.toHaveProperty('passphrase');
|
||||
});
|
||||
|
||||
it('verifies the proxy certificate even when the request opts out of verification', () => {
|
||||
const agent = createProxiedHttpsAgent(PROXY_URL, {
|
||||
...TARGET_TLS,
|
||||
rejectUnauthorized: false,
|
||||
secureOptions: 4,
|
||||
});
|
||||
|
||||
expect(agent.connectOpts).not.toHaveProperty('rejectUnauthorized');
|
||||
expect(agent.connectOpts).not.toHaveProperty('secureOptions');
|
||||
});
|
||||
|
||||
it('keeps a target TLS policy from reshaping the proxy handshake', () => {
|
||||
const agent = createProxiedHttpsAgent(PROXY_URL, {
|
||||
ciphers: 'TARGET_CIPHERS',
|
||||
minVersion: 'TLSv1.1',
|
||||
checkServerIdentity: () => undefined,
|
||||
});
|
||||
|
||||
expect(agent.connectOpts).not.toHaveProperty('ciphers');
|
||||
expect(agent.connectOpts).not.toHaveProperty('minVersion');
|
||||
expect(agent.connectOpts).not.toHaveProperty('checkServerIdentity');
|
||||
});
|
||||
|
||||
it('forwards pool and socket options to the connection to the proxy', () => {
|
||||
const agent = createProxiedHttpsAgent(PROXY_URL, { keepAlive: true, timeout: 5000 });
|
||||
|
||||
expect(agent.connectOpts).toMatchObject({ keepAlive: true, timeout: 5000 });
|
||||
});
|
||||
|
||||
describe('tunnelled session', () => {
|
||||
let parentConnect: MockInstance<HttpsProxyAgent<string>['connect']>;
|
||||
|
||||
beforeEach(() => {
|
||||
parentConnect = vi
|
||||
.spyOn(HttpsProxyAgent.prototype, 'connect')
|
||||
.mockResolvedValue({} as net.Socket);
|
||||
});
|
||||
|
||||
afterEach(() => parentConnect.mockRestore());
|
||||
|
||||
it('carries the target trust material', async () => {
|
||||
const agent = createProxiedHttpsAgent(PROXY_URL, {
|
||||
...TARGET_TLS,
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
await agent.connect({} as http.ClientRequest, targetRequestOpts);
|
||||
|
||||
expect(parentConnect).toHaveBeenCalledWith(
|
||||
{},
|
||||
expect.objectContaining({
|
||||
host: 'target.example.com',
|
||||
port: 443,
|
||||
secureEndpoint: true,
|
||||
ca: 'TARGET_CA',
|
||||
cert: 'TARGET_CERT',
|
||||
key: 'TARGET_KEY',
|
||||
passphrase: 'TARGET_PASSPHRASE',
|
||||
rejectUnauthorized: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves the SNI to be derived from the host of each hop', async () => {
|
||||
const agent = createProxiedHttpsAgent(PROXY_URL, TARGET_TLS);
|
||||
|
||||
await agent.connect({} as http.ClientRequest, targetRequestOpts);
|
||||
|
||||
expect(parentConnect.mock.calls[0][1]).not.toHaveProperty('servername');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProxiedHttpAgent', () => {
|
||||
it('keeps the target SNI and trust material off the connection to the proxy', () => {
|
||||
const agent = createProxiedHttpAgent(PROXY_URL, TARGET_TLS);
|
||||
|
||||
expect(agent.connectOpts).toMatchObject({ host: 'proxy.internal', port: 3128 });
|
||||
expect(agent.connectOpts).not.toHaveProperty('servername');
|
||||
expect(agent.connectOpts).not.toHaveProperty('ca');
|
||||
expect(agent.connectOpts).not.toHaveProperty('cert');
|
||||
expect(agent.connectOpts).not.toHaveProperty('key');
|
||||
expect(agent.connectOpts).not.toHaveProperty('passphrase');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import type http from 'node:http';
|
||||
import type https from 'node:https';
|
||||
import type net from 'node:net';
|
||||
|
||||
/** Per-hop connect options of a proxy agent, as `agent-base` hands them to `connect()`. */
|
||||
type ProxyConnectOpts = Parameters<HttpsProxyAgent<string>['connect']>[1];
|
||||
|
||||
/**
|
||||
* How a socket is opened and pooled, as opposed to how the peer on the other end is
|
||||
* authenticated. Every TLS option in a request describes the target, including the
|
||||
* caller's willingness to accept a certificate it cannot verify, so a proxy borrows
|
||||
* these and nothing else: it is a different peer, and its certificate is always
|
||||
* verified against the proxy hostname. A private proxy CA is trusted through the
|
||||
* process trust store (`NODE_EXTRA_CA_CERTS`), not through a per-request option.
|
||||
*/
|
||||
const CONNECTION_OPTION_KEYS = [
|
||||
'keepAlive',
|
||||
'keepAliveMsecs',
|
||||
'keepAliveInitialDelay',
|
||||
'maxSockets',
|
||||
'maxTotalSockets',
|
||||
'maxFreeSockets',
|
||||
'scheduling',
|
||||
'timeout',
|
||||
'noDelay',
|
||||
'family',
|
||||
'hints',
|
||||
'localAddress',
|
||||
'localPort',
|
||||
'lookup',
|
||||
'autoSelectFamily',
|
||||
'autoSelectFamilyAttemptTimeout',
|
||||
] as const satisfies ReadonlyArray<keyof https.AgentOptions>;
|
||||
|
||||
function pickKeys(agentOptions: https.AgentOptions, keys: readonly string[]): https.AgentOptions {
|
||||
return Object.fromEntries(
|
||||
Object.entries(agentOptions).filter(([key]) => keys.includes(key)),
|
||||
) as https.AgentOptions;
|
||||
}
|
||||
|
||||
function omitKeys(agentOptions: https.AgentOptions, keys: readonly string[]): https.AgentOptions {
|
||||
return Object.fromEntries(
|
||||
Object.entries(agentOptions).filter(([key]) => !keys.includes(key)),
|
||||
) as https.AgentOptions;
|
||||
}
|
||||
|
||||
function forProxyConnection(agentOptions: https.AgentOptions = {}): https.AgentOptions {
|
||||
return pickKeys(agentOptions, CONNECTION_OPTION_KEYS);
|
||||
}
|
||||
|
||||
function forTunnelledConnection(agentOptions: https.AgentOptions = {}): https.AgentOptions {
|
||||
return omitKeys(agentOptions, ['servername']);
|
||||
}
|
||||
|
||||
class TunnellingHttpsProxyAgent extends HttpsProxyAgent<string> {
|
||||
private readonly tunnelOptions: https.AgentOptions;
|
||||
|
||||
constructor(proxyUrl: string, agentOptions?: https.AgentOptions) {
|
||||
super(proxyUrl, forProxyConnection(agentOptions));
|
||||
this.tunnelOptions = forTunnelledConnection(agentOptions);
|
||||
}
|
||||
|
||||
async connect(req: http.ClientRequest, opts: ProxyConnectOpts): Promise<net.Socket> {
|
||||
return await super.connect(req, { ...opts, ...this.tunnelOptions });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the agent routing plain-HTTP targets through `proxyUrl`.
|
||||
* Such a target has no TLS session of its own, so its TLS options are dropped.
|
||||
*
|
||||
* @param agentOptions options describing the connection to the **target**; only its
|
||||
* connection-management options reach the proxy
|
||||
*/
|
||||
export function createProxiedHttpAgent(
|
||||
proxyUrl: string,
|
||||
agentOptions?: https.AgentOptions,
|
||||
): HttpProxyAgent<string> {
|
||||
return new HttpProxyAgent(proxyUrl, forProxyConnection(agentOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the agent tunnelling HTTPS targets through `proxyUrl`, applying the
|
||||
* target's TLS options to the tunnelled session rather than to the proxy handshake.
|
||||
*
|
||||
* @param agentOptions options describing the connection to the **target**; only its
|
||||
* connection-management options reach the proxy
|
||||
*/
|
||||
export function createProxiedHttpsAgent(
|
||||
proxyUrl: string,
|
||||
agentOptions?: https.AgentOptions,
|
||||
): HttpsProxyAgent<string> {
|
||||
return new TunnellingHttpsProxyAgent(proxyUrl, agentOptions);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import http from 'http';
|
||||
import { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import https from 'https';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { getProxyForUrl } from 'proxy-from-env';
|
||||
|
||||
import { createProxiedHttpAgent, createProxiedHttpsAgent } from './proxied-agents';
|
||||
|
||||
/**
|
||||
* Resolves the proxy URL configured via environment variables
|
||||
* (HTTP_PROXY, HTTPS_PROXY, NO_PROXY, etc.) for a given target URL.
|
||||
@@ -40,7 +40,7 @@ export function createHttpProxyAgent(
|
||||
const proxyUrl = customProxyUrl ?? getProxyForUrl(targetUrl);
|
||||
|
||||
if (proxyUrl) {
|
||||
return new HttpProxyAgent(proxyUrl, options);
|
||||
return createProxiedHttpAgent(proxyUrl, options);
|
||||
}
|
||||
|
||||
return new http.Agent(options);
|
||||
@@ -62,7 +62,7 @@ export function createHttpsProxyAgent(
|
||||
const proxyUrl = customProxyUrl ?? getProxyForUrl(targetUrl);
|
||||
|
||||
if (proxyUrl) {
|
||||
return new HttpsProxyAgent(proxyUrl, options);
|
||||
return createProxiedHttpsAgent(proxyUrl, options);
|
||||
}
|
||||
|
||||
return new https.Agent(options);
|
||||
|
||||
@@ -296,7 +296,7 @@ describe('ClientOAuth2', () => {
|
||||
expect(httpsAgent.options.rejectUnauthorized).toBe(false);
|
||||
});
|
||||
|
||||
it('should route through an https proxy agent with relaxed TLS when HTTPS_PROXY is set', async () => {
|
||||
it('should route through an https proxy agent when HTTPS_PROXY is set', async () => {
|
||||
process.env.HTTPS_PROXY = 'http://fake-proxy.example';
|
||||
|
||||
const axiosSpy = vi.spyOn(axios, 'request').mockResolvedValue({
|
||||
@@ -313,7 +313,9 @@ describe('ClientOAuth2', () => {
|
||||
const requestConfig = axiosSpy.mock.calls[0][0];
|
||||
const httpsAgent = requestConfig.httpsAgent as HttpsProxyAgent<string>;
|
||||
expect(httpsAgent).toBeInstanceOf(HttpsProxyAgent);
|
||||
expect(httpsAgent.connectOpts.rejectUnauthorized).toBe(false);
|
||||
// 'Ignore SSL issues' relaxes the tunnelled session to the target; the
|
||||
// proxy is a different peer and keeps its certificate verified.
|
||||
expect(httpsAgent.connectOpts.rejectUnauthorized).toBeUndefined();
|
||||
// The ignore-SSL branch must keep axios's own proxy handling disabled
|
||||
// so routing stays with our agent, not double-proxied.
|
||||
expect(requestConfig.proxy).toBe(false);
|
||||
@@ -518,7 +520,7 @@ describe('ClientOAuth2', () => {
|
||||
expect(ssrfBridge.validateUrl).toHaveBeenCalledWith(new URL(config.accessTokenUri));
|
||||
});
|
||||
|
||||
it('should relax TLS through the proxy agent without a lookup when ignoreSSLIssues is set', async () => {
|
||||
it('should route through the proxy agent without a lookup when ignoreSSLIssues is set', async () => {
|
||||
process.env.HTTPS_PROXY = 'http://fake-proxy.example';
|
||||
const ssrfBridge = makeSsrfBridge();
|
||||
const axiosSpy = proxiedTokenResponse();
|
||||
@@ -527,7 +529,7 @@ describe('ClientOAuth2', () => {
|
||||
|
||||
const httpsAgent = axiosSpy.mock.calls[0][0].httpsAgent as HttpsProxyAgent<string>;
|
||||
expect(httpsAgent).toBeInstanceOf(HttpsProxyAgent);
|
||||
expect(httpsAgent.connectOpts.rejectUnauthorized).toBe(false);
|
||||
expect(httpsAgent.connectOpts.rejectUnauthorized).toBeUndefined();
|
||||
expect(httpsAgent.connectOpts.lookup).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user