`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.
116 lines
4.0 KiB
TypeScript
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)
|
|
})
|