Skip to content

Commit df4f7e0

Browse files
authored
Merge pull request from GHSA-pgw7-wx7w-2w33
* fix: add CONNECT Tunneling HTTP Proxy * test: add proxy agent http(s) tests * refactor: use build connector * refactor: remove secureProxy option * update: remove connection:close * test(proxy-agent): rename tests * test: use proxy/request tls option * types: add proxyTls and requestTls types * refactor: rename socket option to httpSocket * test(proxy-agent): include proxy.on request assert * update: add throwIfProxyAuth function * refactor: remove unnecessary return * refactor: make connect tunneling work with Client.connect * chore: add rsa:2048 key/crt Avoid skipping tests in OpenSSL 3.x (v18). Ref: c5b7d03 * chore: remove self-cert * fix: adjust CONNECT host header * update: use symbol for connectEndpoint
1 parent 2717d70 commit df4f7e0

File tree

9 files changed

+505
-41
lines changed

9 files changed

+505
-41
lines changed

lib/api/api-connect.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class ConnectHandler extends AsyncResource {
1515
throw new InvalidArgumentError('invalid callback')
1616
}
1717

18-
const { signal, opaque, responseHeaders } = opts
18+
const { signal, opaque, responseHeaders, httpTunnel } = opts
1919

2020
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
2121
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
@@ -27,6 +27,7 @@ class ConnectHandler extends AsyncResource {
2727
this.responseHeaders = responseHeaders || null
2828
this.callback = callback
2929
this.abort = null
30+
this.httpTunnel = httpTunnel
3031

3132
addSignal(this, signal)
3233
}
@@ -40,8 +41,23 @@ class ConnectHandler extends AsyncResource {
4041
this.context = context
4142
}
4243

43-
onHeaders () {
44-
throw new SocketError('bad connect', null)
44+
onHeaders (statusCode) {
45+
// when httpTunnel headers are allowed
46+
if (this.httpTunnel) {
47+
const { callback, opaque } = this
48+
if (statusCode !== 200) {
49+
if (callback) {
50+
this.callback = null
51+
const err = new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling')
52+
queueMicrotask(() => {
53+
this.runInAsyncScope(callback, null, err, { opaque })
54+
})
55+
}
56+
return 1
57+
}
58+
} else {
59+
throw new SocketError('bad connect', null)
60+
}
4561
}
4662

4763
onUpgrade (statusCode, rawHeaders, socket) {

lib/core/connect.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
2121
timeout = timeout == null ? 10e3 : timeout
2222
maxCachedSessions = maxCachedSessions == null ? 100 : maxCachedSessions
2323

24-
return function connect ({ hostname, host, protocol, port, servername }, callback) {
24+
return function connect ({ hostname, host, protocol, port, servername, httpSocket }, callback) {
2525
let socket
2626
if (protocol === 'https:') {
2727
if (!tls) {
@@ -39,6 +39,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
3939
...options,
4040
servername,
4141
session,
42+
socket: httpSocket, // upgrade socket connection
4243
port: port || 443,
4344
host: hostname
4445
})
@@ -65,6 +66,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
6566
}
6667
})
6768
} else {
69+
assert(!httpSocket, 'httpSocket can only be sent on TLS update')
6870
socket = net.connect({
6971
highWaterMark: 64 * 1024, // Same as nodejs fs streams.
7072
...options,

lib/core/request.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ class Request {
4848
}, handler) {
4949
if (typeof path !== 'string') {
5050
throw new InvalidArgumentError('path must be a string')
51-
} else if (path[0] !== '/' && !(path.startsWith('http://') || path.startsWith('https://'))) {
51+
} else if (
52+
path[0] !== '/' &&
53+
!(path.startsWith('http://') || path.startsWith('https://')) &&
54+
method !== 'CONNECT'
55+
) {
5256
throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
5357
}
5458

lib/proxy-agent.js

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,84 @@
11
'use strict'
22

33
const { kProxy, kClose, kDestroy } = require('./core/symbols')
4+
const Client = require('./agent')
45
const Agent = require('./agent')
56
const DispatcherBase = require('./dispatcher-base')
67
const { InvalidArgumentError } = require('./core/errors')
8+
const buildConnector = require('./core/connect')
79

810
const kAgent = Symbol('proxy agent')
11+
const kClient = Symbol('proxy client')
12+
const kProxyHeaders = Symbol('proxy headers')
13+
const kRequestTls = Symbol('request tls settings')
14+
const kProxyTls = Symbol('proxy tls settings')
15+
const kConnectEndpoint = Symbol('connect endpoint function')
916

1017
class ProxyAgent extends DispatcherBase {
1118
constructor (opts) {
1219
super(opts)
1320
this[kProxy] = buildProxyOptions(opts)
14-
this[kAgent] = new Agent(opts)
21+
this[kRequestTls] = opts.requestTls
22+
this[kProxyTls] = opts.proxyTls
23+
this[kProxyHeaders] = {}
24+
25+
if (opts.auth) {
26+
this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}`
27+
}
28+
29+
const connect = buildConnector({ ...opts.proxyTls })
30+
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
31+
this[kClient] = new Client({ origin: opts.origin, connect })
32+
this[kAgent] = new Agent({ ...opts, connect: this.connectTunnel.bind(this) })
1533
}
1634

1735
dispatch (opts, handler) {
1836
const { host } = new URL(opts.origin)
37+
const headers = buildHeaders(opts.headers)
38+
throwIfProxyAuthIsSent(headers)
1939
return this[kAgent].dispatch(
2040
{
2141
...opts,
22-
origin: this[kProxy].uri,
23-
path: opts.origin + opts.path,
2442
headers: {
25-
...buildHeaders(opts.headers),
43+
...headers,
2644
host
2745
}
2846
},
2947
handler
3048
)
3149
}
3250

51+
async connectTunnel (opts, callback) {
52+
try {
53+
const { socket } = await this[kClient].connect({
54+
origin: this[kProxy].origin,
55+
port: this[kProxy].port,
56+
path: opts.host,
57+
signal: opts.signal,
58+
headers: {
59+
...this[kProxyHeaders],
60+
host: opts.host
61+
},
62+
httpTunnel: true
63+
})
64+
if (opts.protocol !== 'https:') {
65+
callback(null, socket)
66+
return
67+
}
68+
this[kConnectEndpoint]({ ...opts, servername: this[kRequestTls]?.servername || opts.servername, httpSocket: socket }, callback)
69+
} catch (err) {
70+
callback(err)
71+
}
72+
}
73+
3374
async [kClose] () {
3475
await this[kAgent].close()
76+
await this[kClient].close()
3577
}
3678

3779
async [kDestroy] () {
3880
await this[kAgent].destroy()
81+
await this[kClient].destroy()
3982
}
4083
}
4184

@@ -48,10 +91,7 @@ function buildProxyOptions (opts) {
4891
throw new InvalidArgumentError('Proxy opts.uri is mandatory')
4992
}
5093

51-
return {
52-
uri: opts.uri,
53-
protocol: opts.protocol || 'https'
54-
}
94+
return new URL(opts.uri)
5595
}
5696

5797
/**
@@ -75,4 +115,20 @@ function buildHeaders (headers) {
75115
return headers
76116
}
77117

118+
/**
119+
* @param {Record<string, string>} headers
120+
*
121+
* Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers
122+
* Nevertheless, it was changed and to avoid a security vulnerability by end users
123+
* this check was created.
124+
* It should be removed in the next major version for performance reasons
125+
*/
126+
function throwIfProxyAuthIsSent (headers) {
127+
const existProxyAuth = headers && Object.keys(headers)
128+
.find((key) => key.toLowerCase() === 'proxy-authorization')
129+
if (existProxyAuth) {
130+
throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor')
131+
}
132+
}
133+
78134
module.exports = ProxyAgent

test/fixtures/client-crt-2048.pem

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDkzCCAnugAwIBAgIUF2CLbUCxPnxARRlO7pANiXtZoLIwDQYJKoZIhvcNAQEL
3+
BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
4+
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X
5+
DTIyMDYwOTE0Mzc0N1oXDTI1MDMwNDE0Mzc0N1owWTELMAkGA1UEBhMCQVUxEzAR
6+
BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5
7+
IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
8+
MIIBCgKCAQEA4PbcFnMY0FC1wzsyMf04GhOx/KNcOalHu4Wy76Wys+WoJ6hO5z87
9+
ZIcmsg0hbys1l6DGxloTXeZwcBDoOndUg3FBZvAXRKimhXA7Qf31a9efq9GXic2W
10+
7Kyn1jPa724Vkr/zzlWb5I/Qkk6xcQmEFCDhilbMtpnPz/BwOwn/2vbcbiHNirUk
11+
Dn+s0pUcQlin1f2AR4Jq7/K1xsqjjB6cU0chuzrwzwrglQS7jpXQxCsRaAAIZQJB
12+
DTVQBEo/skqWwv8xABlVQgolxABIX3Wc3RUk7xRItdWCMe92/BJCGhWVXb2hUCBu
13+
y/yz5hX9p353JlxmXEKQlhfPzhcdDv2sdwIDAQABo1MwUTAdBgNVHQ4EFgQUQ0di
14+
dFnBDLhSDgHpM+/KBn+WmI4wHwYDVR0jBBgwFoAUQ0didFnBDLhSDgHpM+/KBn+W
15+
mI4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAoCQJci8G+cUF
16+
n030frY/OgnLJXUGC2ed9Tu+dYhEE7XQkX9WO3IK8As+jGY0kKzX7ZsvWAHHbSa3
17+
8qmHh1vWflU9HEc0MO0Toy6Ale2/BCjs0Oy3q2vd6t9pl3Pq2JTHyJNYu44h45we
18+
ufQ+ttylHGZSmAqeHz4yGp1xVvjbfriDYuc0kW9UTwMpdpzR9RmqQEVD4ySxpuYV
19+
FTj/ZiY89GdIJvsz1pmAhTUcUfuMgSlWS1nt0YR4yMkFS8KqQ1iKEApjrdDCU48W
20+
eABaPeTCUlBCFEDuKxFVPduYVVvOHtkX/8LPH3CO7EDMoSZ1iCDZ7b2+AZbwh9j+
21+
dXqw+WFi7w==
22+
-----END CERTIFICATE-----

test/fixtures/client-key-2048.pem

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEpAIBAAKCAQEA4PbcFnMY0FC1wzsyMf04GhOx/KNcOalHu4Wy76Wys+WoJ6hO
3+
5z87ZIcmsg0hbys1l6DGxloTXeZwcBDoOndUg3FBZvAXRKimhXA7Qf31a9efq9GX
4+
ic2W7Kyn1jPa724Vkr/zzlWb5I/Qkk6xcQmEFCDhilbMtpnPz/BwOwn/2vbcbiHN
5+
irUkDn+s0pUcQlin1f2AR4Jq7/K1xsqjjB6cU0chuzrwzwrglQS7jpXQxCsRaAAI
6+
ZQJBDTVQBEo/skqWwv8xABlVQgolxABIX3Wc3RUk7xRItdWCMe92/BJCGhWVXb2h
7+
UCBuy/yz5hX9p353JlxmXEKQlhfPzhcdDv2sdwIDAQABAoIBAFVfeaCPZ2BO8Nu5
8+
UFBGP48t4EL3H93GDzHsCD8IC+xXgFwkdGUvyvNYkufJMeIFbN4xJp5JusXM2Oi+
9+
kdL2TD1hsqdFAB+PPTqwn9xoa0XU24SSEsc6HUeOMleI8FIi3c8GR5kLRhEUPtv3
10+
P0GdkeEtpUohrKizcHkCTyUoo09N35MFoH3Nb1iyMd10uq0iQlusljkTuukcHstK
11+
MZQAYYcslqzyz9468O/cvsk23Ynd5FfjLgYKmdJ09qaxm4ptnF9NNJ2cLqwElbUF
12+
xI3H5L/t1zxdwI0xZFFgDA4Ccpeq9QsRhRJGAOV94tN+4PxWXEPeQk4PM1EFDrNU
13+
yysi/XkCgYEA+ElKG6cWQZydsb5Tk1vdJ/k18gZa5sv+WUGXkfm9EVecftGjtKQO
14+
c7GwHO1IsLoZkhKfPpa/oifBR97DZRzw1ManEQPS980TZYei3Y9/8uPEpvgvRmm9
15+
MCHA5wp6YMlkZ5VN0SBRWnPhLtZ8L2/cqHOUCQf6YsIJU9/fewufrbUCgYEA5/QU
16+
/tDBDl/f4A2R1HlIkGd1jS//CJLCc3riy0SQxcWIq6/cqflyfvRWiax5DwcO7qfh
17+
3WbJldu9H0IWZjBCqX0v/jHvWBzaKNQCKbFFcL76Lr8bJCwlUMTH9MOhHf3uCOHD
18+
J7YSTVJdvgzLN8K6yFhc0gI4VYQtnQTWJENObPsCgYEAlawAq6jO5uCVw3dbhGKF
19+
cDpwBaVFGQpyGrZKu6nUCudIpL6VtCiNubqs0tNL1ZVqIr9tFdrkTMkwX7XvDj4j
20+
A/F49u3aOJ18iuD4Eh4WYIJjos/MF+NYM/K1CdIsMbpV94dusJmN0Tw3y/dqR2Jk
21+
n3uFCuivTOdxngk//DnmmV0CgYEA1CXNUiZSfLg5xe4DVEc9lD3cKS8d3pSEXySk
22+
6+8hTpHV59moRJpPG0iVIcRq0NDO2n8YOOy7MWJSPpWucPZw8h362E6Jr5hr/G20
23+
MLffYDh8EGdgBpyN4Kqqi/allQ3cOalrWhXP9YKBFMMU10I2nekbtESti6GiKnvy
24+
9CXPRCMCgYBZ2w+VVdhUUBA/elbuEdfbPwIYVDAk31PYg0c9jvQVusmfD1CuY/51
25+
JVsF5oJSosiN7WdDIETkklth0q3lAsQBKoYYMUw54RBf6FawoumB6MVdc3u4y9Ko
26+
l9JC9czdEqb/e0LBqFiWsrtPk9WQf2gyN1mIXQPbyTT1O1J+DvUIbQ==
27+
-----END RSA PRIVATE KEY-----

0 commit comments

Comments
 (0)