Skip to content

Commit d6abdf6

Browse files
committed
fix: auto-detect HTTP proxy tunneling
Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 5c96f7d commit d6abdf6

5 files changed

Lines changed: 66 additions & 7 deletions

File tree

docs/docs/api/ProxyAgent.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ For detailed information on the parsing process and potential validation errors,
2727
* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)`
2828
* **requestTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the request. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
2929
* **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
30-
* **proxyTunnel** `boolean` (optional) - For connections involving secure protocols, Undici will always establish a tunnel via the HTTP2 CONNECT extension. If proxyTunnel is set to true, this will occur for unsecured proxy/endpoint connections as well. Currently, there is no way to facilitate HTTP1 IP tunneling as described in https://www.rfc-editor.org/rfc/rfc9484.html#name-http-11-request. If proxyTunnel is set to false (the default), ProxyAgent connections where both the Proxy and Endpoint are unsecured will issue all requests to the Proxy, and prefix the endpoint request path with the endpoint origin address.
30+
* **proxyTunnel** `boolean` (optional) - Undici automatically detects when proxy tunneling is required. If either the proxy or the target endpoint uses a secure protocol, Undici will establish a tunnel via CONNECT. For plain HTTP proxy to plain HTTP endpoint connections, Undici will forward requests directly to the proxy and prefix the request path with the target origin. Set `proxyTunnel` to `true` to force tunneling for those unsecured HTTP-to-HTTP connections as well. Currently, there is no way to facilitate HTTP1 IP tunneling as described in https://www.rfc-editor.org/rfc/rfc9484.html#name-http-11-request.
3131

3232
Examples:
3333

lib/dispatcher/proxy-agent.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ function defaultAgentFactory (origin, opts) {
3636
return new Pool(origin, opts)
3737
}
3838

39+
function shouldProxyTunnel (proxyProtocol, requestProtocol, proxyTunnel) {
40+
return proxyTunnel === true || proxyProtocol !== 'http:' || requestProtocol !== 'http:'
41+
}
42+
3943
class Http1ProxyWrapper extends DispatcherBase {
4044
#client
4145

@@ -104,7 +108,7 @@ class ProxyAgent extends DispatcherBase {
104108
throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.')
105109
}
106110

107-
const { proxyTunnel = true, connectTimeout } = opts
111+
const { proxyTunnel, connectTimeout } = opts
108112

109113
super()
110114

@@ -150,7 +154,7 @@ class ProxyAgent extends DispatcherBase {
150154
})
151155
}
152156

153-
if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') {
157+
if (!shouldProxyTunnel(this[kProxy].protocol, protocol, this[kTunnelProxy])) {
154158
return new Http1ProxyWrapper(this[kProxy].uri, {
155159
headers: this[kProxyHeaders],
156160
connect,

test/env-http-proxy-agent.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const { tspl } = require('@matteo.collina/tspl')
44
const { test, describe, after, beforeEach } = require('node:test')
55
const { EnvHttpProxyAgent, ProxyAgent, Agent, fetch, MockAgent } = require('..')
66
const { kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent, kClosed, kDestroyed, kProxy } = require('../lib/core/symbols')
7+
const { createServer } = require('node:http')
8+
const { createProxy } = require('proxy')
79

810
const env = { ...process.env }
911

@@ -158,6 +160,54 @@ test('destroys all agents', async (t) => {
158160
t.ok(dispatcher[kHttpsProxyAgent][kDestroyed])
159161
})
160162

163+
test('defaults to non-tunneled HTTP proxying for HTTP endpoints - #5093', async (t) => {
164+
t = tspl(t, { plan: 3 })
165+
166+
const server = await buildServer()
167+
const proxy = await buildProxy()
168+
169+
process.env.http_proxy = `http://localhost:${proxy.address().port}`
170+
171+
const dispatcher = new EnvHttpProxyAgent()
172+
const serverUrl = `http://localhost:${server.address().port}`
173+
174+
try {
175+
proxy.on('connect', () => {
176+
t.fail('should not tunnel plain HTTP over an HTTP proxy by default')
177+
})
178+
179+
proxy.on('request', (req) => {
180+
t.strictEqual(req.url, `${serverUrl}/`)
181+
})
182+
183+
server.on('request', (req, res) => {
184+
t.strictEqual(req.url, '/')
185+
res.end('ok')
186+
})
187+
188+
const response = await fetch(serverUrl, { dispatcher })
189+
t.strictEqual(await response.text(), 'ok')
190+
} finally {
191+
await new Promise((resolve) => proxy.close(resolve))
192+
await new Promise((resolve) => server.close(resolve))
193+
await dispatcher.close()
194+
}
195+
})
196+
197+
function buildServer () {
198+
return new Promise((resolve) => {
199+
const server = createServer({ joinDuplicateHeaders: true })
200+
server.listen(0, () => resolve(server))
201+
})
202+
}
203+
204+
function buildProxy () {
205+
return new Promise((resolve) => {
206+
const server = createProxy(createServer({ joinDuplicateHeaders: true }))
207+
server.listen(0, () => resolve(server))
208+
})
209+
}
210+
161211
const createEnvHttpProxyAgentWithMocks = (plan = 1, opts = {}) => {
162212
const factory = (origin) => {
163213
const mockAgent = new MockAgent()
@@ -171,7 +221,7 @@ const createEnvHttpProxyAgentWithMocks = (plan = 1, opts = {}) => {
171221
}
172222
process.env.http_proxy = 'http://localhost:8080'
173223
process.env.https_proxy = 'http://localhost:8443'
174-
const dispatcher = new EnvHttpProxyAgent({ ...opts, factory })
224+
const dispatcher = new EnvHttpProxyAgent({ proxyTunnel: true, ...opts, factory })
175225
const agentSymbols = [kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent]
176226
agentSymbols.forEach((agentSymbol) => {
177227
const originalDispatch = dispatcher[agentSymbol].dispatch

test/proxy-agent.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,7 @@ test('use proxy-agent with setGlobalDispatcher', async (t) => {
900900

901901
const serverUrl = `http://localhost:${server.address().port}`
902902
const proxyUrl = `http://localhost:${proxy.address().port}`
903-
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
903+
const proxyAgent = new ProxyAgent({ uri: proxyUrl })
904904
const parsedOrigin = new URL(serverUrl)
905905
setGlobalDispatcher(proxyAgent)
906906

@@ -984,7 +984,7 @@ test('ProxyAgent correctly sends headers when using fetch - #1355, #1623', async
984984
const serverUrl = `http://localhost:${server.address().port}`
985985
const proxyUrl = `http://localhost:${proxy.address().port}`
986986

987-
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
987+
const proxyAgent = new ProxyAgent({ uri: proxyUrl })
988988
setGlobalDispatcher(proxyAgent)
989989

990990
after(() => setGlobalDispatcher(defaultDispatcher))
@@ -1094,7 +1094,7 @@ test('should throw when proxy does not return 200', async (t) => {
10941094
return false
10951095
}
10961096

1097-
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
1097+
const proxyAgent = new ProxyAgent({ uri: proxyUrl })
10981098
try {
10991099
await request(serverUrl, { dispatcher: proxyAgent })
11001100
t.fail()

types/proxy-agent.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ declare namespace ProxyAgent {
2424
requestTls?: buildConnector.BuildOptions;
2525
proxyTls?: buildConnector.BuildOptions;
2626
clientFactory?(origin: URL, opts: object): Dispatcher;
27+
/**
28+
* Undici automatically tunnels when either the proxy or the target endpoint
29+
* uses a secure protocol. Set to true to force tunneling for plain HTTP
30+
* proxy to plain HTTP endpoint connections as well.
31+
*/
2732
proxyTunnel?: boolean;
2833
}
2934
}

0 commit comments

Comments
 (0)