@@ -4,7 +4,6 @@ import net = require('net');
4
4
import tls = require( 'tls' ) ;
5
5
import http = require( 'http' ) ;
6
6
import http2 = require( 'http2' ) ;
7
- import * as streams from 'stream' ;
8
7
9
8
import * as semver from 'semver' ;
10
9
import { makeDestroyable , DestroyableServer } from 'destroyable-server' ;
@@ -24,15 +23,17 @@ import { shouldPassThrough } from '../util/server-utils';
24
23
import {
25
24
getParentSocket ,
26
25
buildSocketTimingInfo ,
27
- buildSocketEventData ,
26
+ buildTlsSocketEventData ,
28
27
SocketIsh ,
29
28
InitialRemoteAddress ,
30
29
InitialRemotePort ,
31
30
SocketTimingInfo ,
32
31
LastTunnelAddress ,
33
32
LastHopEncrypted ,
34
33
TlsMetadata ,
35
- TlsSetupCompleted
34
+ TlsSetupCompleted ,
35
+ getAddressAndPort ,
36
+ resetOrDestroy
36
37
} from '../util/socket-util' ;
37
38
import { MockttpHttpsOptions } from '../mockttp' ;
38
39
import { buildSocksServer , SocksTcpAddress } from './socks-server' ;
@@ -62,13 +63,6 @@ const originalSocketInit = (<any>tls.TLSSocket.prototype)._init;
62
63
} ;
63
64
} ;
64
65
65
- export interface ComboServerOptions {
66
- debug : boolean ;
67
- https : MockttpHttpsOptions | undefined ;
68
- http2 : boolean | 'fallback' ;
69
- socks : boolean ;
70
- } ;
71
-
72
66
// Takes an established TLS socket, calls the error listener if it's silently closed
73
67
function ifTlsDropped ( socket : tls . TLSSocket , errorCallback : ( ) => void ) {
74
68
new Promise ( ( resolve , reject ) => {
@@ -139,26 +133,35 @@ function buildTlsError(
139
133
socket : tls . TLSSocket ,
140
134
cause : TlsHandshakeFailure [ 'failureCause' ]
141
135
) : TlsHandshakeFailure {
142
- const eventData = buildSocketEventData ( socket ) as TlsHandshakeFailure ;
136
+ const eventData = buildTlsSocketEventData ( socket ) as TlsHandshakeFailure ;
143
137
144
138
eventData . failureCause = cause ;
145
139
eventData . timingEvents . failureTimestamp = now ( ) ;
146
140
147
141
return eventData ;
148
142
}
149
143
144
+ export interface ComboServerOptions {
145
+ debug : boolean ;
146
+ https : MockttpHttpsOptions | undefined ;
147
+ http2 : boolean | 'fallback' ;
148
+ socks : boolean ;
149
+ passthroughUnknownProtocols : boolean ;
150
+
151
+ requestListener : ( req : http . IncomingMessage , res : http . ServerResponse ) => void ;
152
+ tlsClientErrorListener : ( socket : tls . TLSSocket , req : TlsHandshakeFailure ) => void ;
153
+ tlsPassthroughListener : ( socket : net . Socket , address : string , port ?: number ) => void ;
154
+ rawPassthroughListener : ( socket : net . Socket , address : string , port ?: number ) => void ;
155
+ } ;
156
+
150
157
// The low-level server that handles all the sockets & TLS. The server will correctly call the
151
158
// given handler for both HTTP & HTTPS direct connections, or connections when used as an
152
159
// either HTTP or HTTPS proxy, all on the same port.
153
- export async function createComboServer (
154
- options : ComboServerOptions ,
155
- requestListener : ( req : http . IncomingMessage , res : http . ServerResponse ) => void ,
156
- tlsClientErrorListener : ( socket : tls . TLSSocket , req : TlsHandshakeFailure ) => void ,
157
- tlsPassthroughListener : ( socket : net . Socket , address : string , port ?: number ) => void
158
- ) : Promise < DestroyableServer < net . Server > > {
160
+ export async function createComboServer ( options : ComboServerOptions ) : Promise < DestroyableServer < net . Server > > {
159
161
let server : net . Server ;
160
162
let tlsServer : tls . Server | undefined = undefined ;
161
163
let socksServer : net . Server | undefined = undefined ;
164
+ let unknownProtocolServer : net . Server | undefined = undefined ;
162
165
163
166
if ( options . https ) {
164
167
const ca = await getCA ( options . https ) ;
@@ -217,7 +220,7 @@ export async function createComboServer(
217
220
tlsServer ,
218
221
options . https . tlsPassthrough ,
219
222
options . https . tlsInterceptOnly ,
220
- tlsPassthroughListener
223
+ options . tlsPassthroughListener
221
224
) ;
222
225
}
223
226
@@ -243,10 +246,29 @@ export async function createComboServer(
243
246
} ) ;
244
247
}
245
248
249
+ if ( options . passthroughUnknownProtocols ) {
250
+ unknownProtocolServer = net . createServer ( ( socket ) => {
251
+ const destination = socket [ LastTunnelAddress ] ;
252
+ if ( ! destination ) {
253
+ server . emit ( 'clientError' , new Error ( 'Unknown protocol without destination' ) , socket ) ;
254
+ return ;
255
+ }
256
+
257
+ const [ host , port ] = getAddressAndPort ( destination ) ;
258
+ if ( ! port ) { // Both CONNECT & SOCKS require a port, so this shouldn't happen
259
+ server . emit ( 'clientError' , new Error ( 'Unknown protocol without destination port' ) , socket ) ;
260
+ return ;
261
+ }
262
+
263
+ options . rawPassthroughListener ( socket , host , port ) ;
264
+ } ) ;
265
+ }
266
+
246
267
server = httpolyglot . createServer ( {
247
268
tls : tlsServer ,
248
269
socks : socksServer ,
249
- } , requestListener ) ;
270
+ unknownProtocol : unknownProtocolServer
271
+ } , options . requestListener ) ;
250
272
251
273
// In Node v20, this option was added, rejecting all requests with no host header. While that's good, in
252
274
// our case, we want to handle the garbage requests too, so we disable it:
@@ -282,7 +304,7 @@ export async function createComboServer(
282
304
283
305
socket [ LastHopEncrypted ] = true ;
284
306
ifTlsDropped ( socket , ( ) => {
285
- tlsClientErrorListener ( socket , buildTlsError ( socket , 'closed' ) ) ;
307
+ options . tlsClientErrorListener ( socket , buildTlsError ( socket , 'closed' ) ) ;
286
308
} ) ;
287
309
} ) ;
288
310
@@ -295,7 +317,7 @@ export async function createComboServer(
295
317
} ) ;
296
318
297
319
server . on ( 'tlsClientError' , ( error : Error , socket : tls . TLSSocket ) => {
298
- tlsClientErrorListener ( socket , buildTlsError ( socket , getCauseFromError ( error ) ) ) ;
320
+ options . tlsClientErrorListener ( socket , buildTlsError ( socket , getCauseFromError ( error ) ) ) ;
299
321
} ) ;
300
322
301
323
// If the server receives a HTTP/HTTPS CONNECT request, Pretend to tunnel, then just re-handle:
@@ -431,30 +453,23 @@ function analyzeAndMaybePassThroughTls(
431
453
432
454
// SNI is a good clue for where the request is headed, but an explicit proxy address (via
433
455
// CONNECT or SOCKS) is even better. Note that this may be a hostname or IPv4/6 address:
434
- let connectHostname : string | undefined ;
435
- let connectPort : string | undefined ;
456
+ let upstreamHostname : string | undefined ;
457
+ let upstreamPort : number | undefined ;
436
458
if ( socket [ LastTunnelAddress ] ) {
437
- const lastColonIndex = socket [ LastTunnelAddress ] . lastIndexOf ( ':' ) ;
438
- if ( lastColonIndex !== - 1 ) {
439
- connectHostname = socket [ LastTunnelAddress ] . slice ( 0 , lastColonIndex ) ;
440
- connectPort = socket [ LastTunnelAddress ] . slice ( lastColonIndex + 1 ) ;
441
- } else {
442
- connectHostname = socket [ LastTunnelAddress ] ;
443
- }
459
+ ( [ upstreamHostname , upstreamPort ] = getAddressAndPort ( socket [ LastTunnelAddress ] ) ) ;
444
460
}
445
461
446
462
socket [ TlsMetadata ] = {
447
463
sniHostname,
448
- connectHostname,
449
- connectPort,
464
+ connectHostname : upstreamHostname ,
465
+ connectPort : upstreamPort ?. toString ( ) ,
450
466
clientAlpn : helloData . alpnProtocols ,
451
467
ja3Fingerprint : calculateJa3FromFingerprintData ( helloData . fingerprintData ) ,
452
468
ja4Fingerprint : calculateJa4FromHelloData ( helloData )
453
469
} ;
454
470
455
- if ( shouldPassThrough ( connectHostname , passThroughPatterns , interceptOnlyPatterns ) ) {
456
- const upstreamPort = connectPort ? parseInt ( connectPort , 10 ) : undefined ;
457
- passthroughListener ( socket , connectHostname , upstreamPort ) ;
471
+ if ( shouldPassThrough ( upstreamHostname , passThroughPatterns , interceptOnlyPatterns ) ) {
472
+ passthroughListener ( socket , upstreamHostname , upstreamPort ) ;
458
473
return ; // Do not continue with TLS
459
474
} else if ( shouldPassThrough ( sniHostname , passThroughPatterns , interceptOnlyPatterns ) ) {
460
475
passthroughListener ( socket , sniHostname ! ) ; // Can't guess the port - not included in SNI
0 commit comments