Skip to content

Commit 0180741

Browse files
committed
feat(ProxyAgent) improve Curl-y behavior in HTTP->HTTP Proxy connections (nodejs#4180)
This refactors the way the legacy unsecured behaviour is implemented, by wrapping the Proxy client in a wrapper which rewrites requests, and handles errors. This will also insert authentication headers in each request.
1 parent 4608ef1 commit 0180741

File tree

3 files changed

+411
-86
lines changed

3 files changed

+411
-86
lines changed

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) - By default, ProxyAgent will request that the Proxy facilitate a tunnel between the endpoint and the agent. Setting `proxyTunnel` to false avoids issuing a CONNECT extension, and includes the endpoint domain and path in each request.
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.
3131

3232
Examples:
3333

lib/dispatcher/proxy-agent.js

Lines changed: 67 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict'
22

3-
const { kProxy, kClose, kDestroy, kDispatch, kConnector } = require('../core/symbols')
3+
const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols')
44
const { URL } = require('node:url')
55
const Agent = require('./agent')
66
const Pool = require('./pool')
@@ -27,61 +27,69 @@ function defaultFactory (origin, opts) {
2727

2828
const noop = () => {}
2929

30-
class ProxyClient extends DispatcherBase {
31-
#client = null
32-
constructor (origin, opts) {
33-
if (typeof origin === 'string') {
34-
origin = new URL(origin)
35-
}
30+
function defaultAgentFactory (origin, opts) {
31+
if (opts.connections === 1) {
32+
return new Client(origin, opts)
33+
}
34+
return new Pool(origin, opts)
35+
}
3636

37-
if (origin.protocol !== 'http:' && origin.protocol !== 'https:') {
38-
throw new InvalidArgumentError('ProxyClient only supports http and https protocols')
39-
}
37+
class Http1ProxyWrapper extends DispatcherBase {
38+
#client
4039

40+
constructor (proxyUrl, { headers = {}, connect, factory }) {
4141
super()
42+
if (!proxyUrl) {
43+
throw new InvalidArgumentError('Proxy URL is mandatory')
44+
}
4245

43-
this.#client = new Client(origin, opts)
44-
}
45-
46-
async [kClose] () {
47-
await this.#client.close()
48-
}
49-
50-
async [kDestroy] () {
51-
await this.#client.destroy()
46+
this[kProxyHeaders] = headers
47+
if (factory) {
48+
this.#client = factory(proxyUrl, { connect })
49+
} else {
50+
this.#client = new Client(proxyUrl, { connect })
51+
}
5252
}
5353

54-
async [kDispatch] (opts, handler) {
55-
const { method, origin } = opts
56-
if (method === 'CONNECT') {
57-
this.#client[kConnector]({
58-
origin,
59-
port: opts.port || defaultProtocolPort(opts.protocol),
60-
path: opts.host,
61-
signal: opts.signal,
62-
headers: {
63-
...this[kProxyHeaders],
64-
host: opts.host
65-
},
66-
servername: this[kProxyTls]?.servername || opts.servername
67-
},
68-
(err, socket) => {
69-
if (err) {
70-
handler.callback(err)
71-
} else {
72-
handler.callback(null, { socket, statusCode: 200 })
54+
[kDispatch] (opts, handler) {
55+
const onHeaders = handler.onHeaders
56+
handler.onHeaders = function (statusCode, data, resume) {
57+
if (statusCode === 407) {
58+
if (typeof handler.onError === 'function') {
59+
handler.onError(new InvalidArgumentError('Proxy Authentication Required (407)'))
7360
}
61+
return
7462
}
75-
)
76-
return
63+
if (onHeaders) onHeaders.call(this, statusCode, data, resume)
7764
}
78-
if (typeof origin === 'string') {
79-
opts.origin = new URL(origin)
65+
66+
// Rewrite request as an HTTP1 Proxy request, without tunneling.
67+
const {
68+
origin,
69+
path = '/',
70+
headers = {}
71+
} = opts
72+
73+
opts.path = origin + path
74+
75+
if (!('host' in headers) && !('Host' in headers)) {
76+
const { host } = new URL(origin)
77+
headers.host = host
8078
}
79+
opts.headers = { ...this[kProxyHeaders], ...headers }
80+
81+
return this.#client[kDispatch](opts, handler)
82+
}
8183

82-
return this.#client.dispatch(opts, handler)
84+
async [kClose] () {
85+
return this.#client.close()
86+
}
87+
88+
async [kDestroy] (err) {
89+
return this.#client.destroy(err)
8390
}
8491
}
92+
8593
class ProxyAgent extends DispatcherBase {
8694
constructor (opts) {
8795
if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
@@ -104,6 +112,7 @@ class ProxyAgent extends DispatcherBase {
104112
this[kRequestTls] = opts.requestTls
105113
this[kProxyTls] = opts.proxyTls
106114
this[kProxyHeaders] = opts.headers || {}
115+
this[kTunnelProxy] = proxyTunnel
107116

108117
if (opts.auth && opts.token) {
109118
throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
@@ -116,21 +125,25 @@ class ProxyAgent extends DispatcherBase {
116125
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
117126
}
118127

119-
const factory = (!proxyTunnel && protocol === 'http:')
120-
? (origin, options) => {
121-
if (origin.protocol === 'http:') {
122-
return new ProxyClient(origin, options)
123-
}
124-
return new Client(origin, options)
125-
}
126-
: undefined
127-
128128
const connect = buildConnector({ ...opts.proxyTls })
129129
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
130-
this[kClient] = clientFactory(url, { connect, factory })
131-
this[kTunnelProxy] = proxyTunnel
130+
131+
const agentFactory = opts.factory || defaultAgentFactory
132+
const factory = (origin, options) => {
133+
const { protocol } = new URL(origin)
134+
if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') {
135+
return new Http1ProxyWrapper(this[kProxy].uri, {
136+
headers: this[kProxyHeaders],
137+
connect,
138+
factory: agentFactory
139+
})
140+
}
141+
return agentFactory(origin, options)
142+
}
143+
this[kClient] = clientFactory(url, { connect })
132144
this[kAgent] = new Agent({
133145
...opts,
146+
factory,
134147
connect: async (opts, callback) => {
135148
let requestedPath = opts.host
136149
if (!opts.port) {
@@ -185,10 +198,6 @@ class ProxyAgent extends DispatcherBase {
185198
headers.host = host
186199
}
187200

188-
if (!this.#shouldConnect(new URL(opts.origin))) {
189-
opts.path = opts.origin + opts.path
190-
}
191-
192201
return this[kAgent].dispatch(
193202
{
194203
...opts,
@@ -221,19 +230,6 @@ class ProxyAgent extends DispatcherBase {
221230
await this[kAgent].destroy()
222231
await this[kClient].destroy()
223232
}
224-
225-
#shouldConnect (uri) {
226-
if (typeof uri === 'string') {
227-
uri = new URL(uri)
228-
}
229-
if (this[kTunnelProxy]) {
230-
return true
231-
}
232-
if (uri.protocol !== 'http:' || this[kProxy].protocol !== 'http:') {
233-
return true
234-
}
235-
return false
236-
}
237233
}
238234

239235
/**

0 commit comments

Comments
 (0)