Files
pnpm/pnpm11/network/proxy-agent/test/proxy-agent.test.ts
Zoltan Kochan 48b1e5425c fix(network): cache agents by their effective settings (#16224)
`getAgent` and `getProxyAgent` keyed their agent caches on
`Boolean(strictSsl)`, which maps an omitted strictSsl and an explicit
false to the same key. Node.js verifies certificates when
rejectUnauthorized is omitted, so an insecure agent cached first was
handed to later callers that expected verification. The keys also left
out maxSockets and timeout, so a caller got an agent with another
caller's connection settings.

Both factories now resolve strictSsl, maxSockets, and timeout to their
effective values once, and use those values in the key and the agent.

`pickSettingByUrl` checked candidate keys by truthiness and skipped
settings whose value is false, 0, or an empty string. It now checks
whether the key exists. Its port-stripping fallback appended the slash
after the query string, which turned `/path?a=1` into a URL whose path
was lost when mapped to a registry key. The slash now goes on the path.

None of these packages is used by the pnpm CLI.
2026-09-27 00:30:55 +02:00

116 lines
4.0 KiB
TypeScript

import { expect, test } from '@jest/globals'
import { getProxyAgent } from '@pnpm/network.proxy-agent'
import { SocksProxyAgent } from 'socks-proxy-agent'
type HttpProxyAgentInternals = HttpsProxyAgentInternals
interface HttpsProxyAgentInternals {
connectOpts: unknown
proxy: URL
}
const OPTS = {
agent: null,
ca: 'ca',
cert: 'cert',
key: 'key',
localAddress: 'localAddress',
maxSockets: 5,
strictSsl: true,
timeout: 5,
}
test('all expected options passed down to proxy agent', () => {
const opts = {
httpsProxy: 'https://user:pass@my.proxy:1234/foo/',
noProxy: 'qar.com, bar.com',
...OPTS,
}
const agent = getProxyAgent('https://foo.com/bar', opts) as unknown as HttpsProxyAgentInternals
expect(agent.connectOpts).toEqual({
ALPNProtocols: ['http/1.1'],
auth: 'user:pass',
ca: 'ca',
cert: 'cert',
host: 'my.proxy',
key: 'key',
localAddress: 'localAddress',
maxSockets: 5,
port: 1234,
rejectUnauthorized: true,
timeout: 6,
})
expect(agent.proxy.protocol).toBe('https:')
})
test('a socks proxy', () => {
const opts = {
httpsProxy: 'socks://user:pass@my.proxy:1234/foo',
...OPTS,
}
const agent = getProxyAgent('https://foo.com/bar', opts)
expect(agent instanceof SocksProxyAgent).toBeTruthy()
expect((agent as SocksProxyAgent).proxy).toEqual({
host: 'my.proxy',
port: 1234,
type: 5,
})
})
test('proxy credentials are decoded', () => {
const opts = {
httpsProxy: `https://${encodeURIComponent('use@!r')}:${encodeURIComponent('p#as*s')}@my.proxy:1234/foo`,
...OPTS,
}
const agent = getProxyAgent('https://foo.com/bar', opts) as unknown as HttpsProxyAgentInternals
expect(agent.connectOpts).toEqual({
ALPNProtocols: ['http/1.1'],
auth: 'use@!r:p#as*s',
ca: 'ca',
cert: 'cert',
host: 'my.proxy',
key: 'key',
localAddress: 'localAddress',
maxSockets: 5,
port: 1234,
// protocol: 'https:',
rejectUnauthorized: true,
timeout: 6,
})
expect(agent.proxy.protocol).toBe('https:')
})
test('proxy credentials that are not URL-encoded are rejected', () => {
const opts = {
httpsProxy: 'https://use@!r:p#as*s@my.proxy:1234/foo',
...OPTS,
}
expect(() => getProxyAgent('https://foo.com/bar', opts)).toThrow("Couldn't parse proxy URL")
})
test('an omitted strictSsl does not reuse the proxy agent created for strictSsl: false', () => {
const httpsProxy = 'https://strict-ssl.proxy:1234'
const insecure = getProxyAgent('https://foo.com/bar', { httpsProxy, strictSsl: false }) as unknown as HttpsProxyAgentInternals
const secure = getProxyAgent('https://foo.com/bar', { httpsProxy }) as unknown as HttpsProxyAgentInternals
expect(insecure.connectOpts).toHaveProperty('rejectUnauthorized', false)
expect(secure.connectOpts).toHaveProperty('rejectUnauthorized', true)
})
test('proxy agents with different connection settings are not shared', () => {
const httpsProxy = 'https://settings.proxy:1234'
const getConnectOpts = (opts: { maxSockets?: number, timeout?: number }) =>
(getProxyAgent('https://foo.com/bar', { httpsProxy, ...opts }) as unknown as HttpsProxyAgentInternals).connectOpts
expect(getConnectOpts({ maxSockets: 1 })).toHaveProperty('maxSockets', 1)
expect(getConnectOpts({ maxSockets: 2 })).toHaveProperty('maxSockets', 2)
expect(getConnectOpts({ timeout: 1 })).toHaveProperty('timeout', 2)
expect(getConnectOpts({ timeout: 2 })).toHaveProperty('timeout', 3)
})
test('an http: destination behind an https: proxy does not reuse the agent created for strictSsl: false', () => {
const httpsProxy = 'https://http-destination.proxy:1234'
const insecure = getProxyAgent('http://foo.com/bar', { httpProxy: httpsProxy, strictSsl: false }) as unknown as HttpProxyAgentInternals
const secure = getProxyAgent('http://foo.com/bar', { httpProxy: httpsProxy }) as unknown as HttpProxyAgentInternals
expect(insecure.connectOpts).toHaveProperty('rejectUnauthorized', false)
expect(secure.connectOpts).toHaveProperty('rejectUnauthorized', true)
})