Skip to content

Commit f170d4d

Browse files
committed
Part 2
This version tries to expose less sketchiness -- it's not particularly well organized yet, and I'm sure it could be cleaned up a lot. Instead of adding the "rawSocket" stuff to RequestOptions, there's a new wrapper ProxyClient added, which intercepts the CONNECT message and prevents it from being dispatched. Unfortunately the wrapper client isn't quite written in a way to manage all of the client-ness, so ProxyAgent is still responsible for updating the PATH of HTTP->HTTP Proxy requests to include the endpoint domain. It is messy though, admittedly.
1 parent abbaba6 commit f170d4d

File tree

3 files changed

+84
-23
lines changed

3 files changed

+84
-23
lines changed

lib/core/request.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ class Request {
4242
reset,
4343
expectContinue,
4444
servername,
45-
throwOnError,
46-
rawSocket
45+
throwOnError
4746
}, handler) {
4847
if (typeof path !== 'string') {
4948
throw new InvalidArgumentError('path must be a string')
@@ -95,12 +94,6 @@ class Request {
9594

9695
this.abort = null
9796

98-
if (rawSocket) {
99-
assert(body == null, 'rawSocket cannot be used with body')
100-
assert(method === 'CONNECT', 'rawSocket can only be used with CONNECT method')
101-
}
102-
this.rawSocket = rawSocket
103-
10497
if (body == null) {
10598
this.body = null
10699
} else if (isStream(body)) {

lib/dispatcher/client.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const {
5151
kOnError,
5252
kHTTPContext,
5353
kMaxConcurrentStreams,
54-
kResume, kSocket
54+
kResume
5555
} = require('../core/symbols.js')
5656
const connectH1 = require('./client-h1.js')
5757
const connectH2 = require('./client-h2.js')
@@ -598,12 +598,6 @@ function _resume (client, sync) {
598598
return
599599
}
600600

601-
if (!request.aborted && request.rawSocket) {
602-
assert(request.method === 'CONNECT')
603-
request.onUpgrade(200, [], client[kSocket])
604-
request.aborted = true
605-
}
606-
607601
if (!request.aborted && client[kHTTPContext].write(request)) {
608602
client[kPendingIdx]++
609603
} else {

lib/dispatcher/proxy-agent.js

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use strict'
22

3-
const { kProxy, kClose, kDestroy } = require('../core/symbols')
3+
const { kProxy, kClose, kDestroy, kDispatch, kConnector } = require('../core/symbols')
44
const { URL } = require('node:url')
55
const Agent = require('./agent')
66
const Pool = require('./pool')
77
const DispatcherBase = require('./dispatcher-base')
88
const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors')
99
const buildConnector = require('../core/connect')
10+
const Client = require('./client')
1011

1112
const kAgent = Symbol('proxy agent')
1213
const kClient = Symbol('proxy client')
@@ -26,6 +27,61 @@ function defaultFactory (origin, opts) {
2627

2728
const noop = () => {}
2829

30+
class ProxyClient extends DispatcherBase {
31+
#client = null
32+
constructor (origin, opts) {
33+
if (typeof origin === 'string') {
34+
origin = new URL(origin)
35+
}
36+
37+
if (origin.protocol !== 'http:' && origin.protocol !== 'https:') {
38+
throw new InvalidArgumentError('ProxyClient only supports http and https protocols')
39+
}
40+
41+
super()
42+
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()
52+
}
53+
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 })
73+
}
74+
}
75+
)
76+
return
77+
}
78+
if (typeof origin === 'string') {
79+
opts.origin = new URL(origin)
80+
}
81+
82+
return this.#client.dispatch(opts, handler)
83+
}
84+
}
2985
class ProxyAgent extends DispatcherBase {
3086
constructor (opts) {
3187
if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
@@ -60,9 +116,18 @@ class ProxyAgent extends DispatcherBase {
60116
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
61117
}
62118

119+
const factory = (!tunnelProxy && 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+
63128
const connect = buildConnector({ ...opts.proxyTls })
64129
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
65-
this[kClient] = clientFactory(url, { connect })
130+
this[kClient] = clientFactory(url, { connect, factory })
66131
this[kTunnelProxy] = tunnelProxy
67132
this[kAgent] = new Agent({
68133
...opts,
@@ -71,7 +136,6 @@ class ProxyAgent extends DispatcherBase {
71136
if (!opts.port) {
72137
requestedPath += `:${defaultProtocolPort(opts.protocol)}`
73138
}
74-
const shouldConnect = this[kTunnelProxy] || protocol !== 'http:' || opts.protocol !== 'http:'
75139
try {
76140
const { socket, statusCode } = await this[kClient].connect({
77141
origin,
@@ -82,7 +146,6 @@ class ProxyAgent extends DispatcherBase {
82146
...this[kProxyHeaders],
83147
host: opts.host
84148
},
85-
rawSocket: !shouldConnect,
86149
servername: this[kProxyTls]?.servername || proxyHostname
87150
})
88151
if (statusCode !== 200) {
@@ -121,10 +184,8 @@ class ProxyAgent extends DispatcherBase {
121184
headers.host = host
122185
}
123186

124-
// If we have a non-secure, non-tunneling proxy, the server URL is prepended to the
125-
// path to ensure that the Proxy can handle the request appropriately.
126-
if (!this[kTunnelProxy] && opts.path && opts.origin) {
127-
opts.path = `${opts.origin}${opts.path}`
187+
if (!this.#shouldConnect(new URL(opts.origin))) {
188+
opts.path = opts.origin + opts.path
128189
}
129190

130191
return this[kAgent].dispatch(
@@ -159,6 +220,19 @@ class ProxyAgent extends DispatcherBase {
159220
await this[kAgent].destroy()
160221
await this[kClient].destroy()
161222
}
223+
224+
#shouldConnect (uri) {
225+
if (typeof uri === 'string') {
226+
uri = new URL(uri)
227+
}
228+
if (this[kTunnelProxy]) {
229+
return true
230+
}
231+
if (uri.protocol !== 'http:' || this[kProxy].protocol !== 'http:') {
232+
return true
233+
}
234+
return false
235+
}
162236
}
163237

164238
/**

0 commit comments

Comments
 (0)