@@ -19,7 +19,9 @@ import { isMockttpBody, encodeBodyBuffer } from '../util/request-utils';
1919import { areFFDHECurvesSupported } from '../util/openssl-compat' ;
2020import { findRawHeaderIndex , getHeaderValue } from '../util/header-utils' ;
2121import { getDefaultPort } from '../util/url' ;
22- import { TlsMetadata } from '../util/socket-extensions' ;
22+ import { TlsMetadata , TlsClientHello } from '../util/socket-extensions' ;
23+ import { buildTlsImpersonationConfig } from '../util/tls-impersonation' ;
24+ import type { Connection } from './http-agents' ;
2325
2426import {
2527 CallbackRequestResult ,
@@ -37,6 +39,11 @@ import { applyMatchReplace } from './match-replace';
3739// issues so far as possible, by closely emulating a Firefox Client Hello:
3840const NEW_CURVES_SUPPORTED = areFFDHECurvesSupported ( process . versions . openssl ) ;
3941
42+ // Trust intermediate certificates from the trusted CA list too. Without this, trusted CAs
43+ // are only used when they are self-signed root certificates. Seems to cause issues in Node v20
44+ // in HTTP/2 tests, so disabled below the supported v22 version.
45+ const allowPartialTrustChain = semver . satisfies ( process . version , '>=22.9.0' ) ;
46+
4047const SSL_OP_LEGACY_SERVER_CONNECT = 1 << 2 ;
4148const SSL_OP_TLSEXT_PADDING = 1 << 4 ;
4249const SSL_OP_NO_ENCRYPT_THEN_MAC = 1 << 19 ;
@@ -49,7 +56,10 @@ export function getUpstreamTlsOptions({
4956
5057 ignoreHostHttpsErrors,
5158 clientCertificateHostMap,
52- trustedCAs
59+ trustedCAs,
60+
61+ connection,
62+ tryHttp2Upstream
5363} : {
5464 // The effective hostname & port we're connecting to - note that this isn't exactly
5565 // the same as the destination (e.g. if you tunnel to an IP but set a hostname via SNI
@@ -60,7 +70,13 @@ export function getUpstreamTlsOptions({
6070 // The general config that's relevant to this request:
6171 ignoreHostHttpsErrors : string [ ] | boolean ,
6272 clientCertificateHostMap : { [ host : string ] : { pfx : Buffer , passphrase ?: string } } ,
63- trustedCAs : Array < string > | undefined
73+ trustedCAs : Array < string > | undefined ,
74+
75+ // The downstream connection, used to mirror client hellos.
76+ connection ?: Connection ,
77+
78+ // Whether we're going to attempt HTTP/2 upstream, required to control our ALPN configuration.
79+ tryHttp2Upstream ?: boolean
6480} ) : tls . ConnectionOptions {
6581 const strictHttpsChecks = shouldUseStrictHttps ( hostname , port , ignoreHostHttpsErrors ) ;
6682
@@ -70,12 +86,53 @@ export function getUpstreamTlsOptions({
7086 clientCertificateHostMap [ '*' ] ||
7187 { } ;
7288
73- return {
89+ // Allow connecting to old servers that don't support secure renegotiation, iff not strict:
90+ const maybeLegacyConnect = strictHttpsChecks ? 0 : SSL_OP_LEGACY_SERVER_CONNECT ;
91+
92+ const trustOptions : tls . SecureContextOptions = {
93+ allowPartialTrustChain,
94+ ...( trustedCAs ? { ca : trustedCAs } : { } ) ,
95+ ...clientCert
96+ } ;
97+
98+ const connectionOptions : tls . ConnectionOptions = {
7499 servername : hostname && ! isIP ( hostname )
75100 ? hostname
76101 : undefined , // Can't send IPs in SNI
102+ rejectUnauthorized : strictHttpsChecks
103+ } ;
104+
105+ // If the connection carries an inbound client hello to mirror, reproduce its TLS fingerprint
106+ // upstream instead of our default.
107+ const clientHello = connection ?. [ TlsClientHello ] ;
108+ if ( connection && clientHello ) {
109+ const impersonationConfig = buildTlsImpersonationConfig ( connection , clientHello , {
110+ ...trustOptions ,
111+ ...( maybeLegacyConnect ? { secureOptions : maybeLegacyConnect } : { } ) ,
112+ security : strictHttpsChecks ? 'secure' : 'insecure'
113+ } ) ;
114+
115+ // This should only be undefined if impersonation isn't supported:
116+ if ( impersonationConfig ) {
117+ // Mirror the client's ALPN, filtered to protocols we want upstream.
118+ // N.b. http2-wrapper currently overrides ALPN on the H2 path regardless (for now).
119+ const speakableUpstreamAlpn = tryHttp2Upstream ? [ 'h2' , 'http/1.1' ] : [ 'http/1.1' ] ;
120+ const mirroredAlpn = impersonationConfig . tlsOptions . ALPNProtocols
121+ ?. filter ( p => speakableUpstreamAlpn . includes ( p ) ) ;
122+
123+ return {
124+ ...connectionOptions ,
125+ ...impersonationConfig . tlsOptions ,
126+ ALPNProtocols : mirroredAlpn ?. length ? mirroredAlpn : undefined
127+ } ;
128+ }
129+ }
130+
131+ // Default fingerprint, emulating a Firefox v103 client hello to limit TLS fingerprinting issues:
132+ return {
133+ ...connectionOptions ,
134+ ...trustOptions ,
77135
78- // We precisely control the various TLS parameters here to limit TLS fingerprinting issues:
79136 ecdhCurve : [
80137 'X25519' ,
81138 'prime256v1' , // N.B. Equivalent to secp256r1
@@ -127,32 +184,11 @@ export function getUpstreamTlsOptions({
127184 : [ ]
128185 )
129186 ] . join ( ':' ) ,
130- secureOptions : strictHttpsChecks
131- ? SSL_OP_TLSEXT_PADDING | SSL_OP_NO_ENCRYPT_THEN_MAC
132- : SSL_OP_TLSEXT_PADDING | SSL_OP_NO_ENCRYPT_THEN_MAC | SSL_OP_LEGACY_SERVER_CONNECT ,
133- ...( {
134- // Valid, but not included in Node.js TLS module types:
135- requestOSCP : true
136- } as any ) ,
137-
138- // Trust intermediate certificates from the trusted CA list too. Without this, trusted CAs
139- // are only used when they are self-signed root certificates. Seems to cause issues in Node v20
140- // in HTTP/2 tests, so disabled below the supported v22 version.
141- allowPartialTrustChain : semver . satisfies ( process . version , '>=22.9.0' ) ,
187+ secureOptions : SSL_OP_TLSEXT_PADDING | SSL_OP_NO_ENCRYPT_THEN_MAC | maybeLegacyConnect ,
188+ requestOCSP : true ,
142189
143190 // Allow TLSv1, if !strict:
144- minVersion : strictHttpsChecks ? tls . DEFAULT_MIN_VERSION : 'TLSv1' ,
145-
146- // Skip certificate validation entirely, if not strict:
147- rejectUnauthorized : strictHttpsChecks ,
148-
149- // Override the set of trusted CAs, if configured to do so:
150- ...( trustedCAs ? {
151- ca : trustedCAs
152- } : { } ) ,
153-
154- // Use a client cert, if one matches for this hostname+port:
155- ...clientCert
191+ minVersion : strictHttpsChecks ? tls . DEFAULT_MIN_VERSION : 'TLSv1'
156192 }
157193}
158194
0 commit comments