@@ -4,7 +4,6 @@ import net = require('net');
44import tls = require( 'tls' ) ;
55import http = require( 'http' ) ;
66import http2 = require( 'http2' ) ;
7- import * as streams from 'stream' ;
87
98import * as semver from 'semver' ;
109import { makeDestroyable , DestroyableServer } from 'destroyable-server' ;
@@ -24,15 +23,17 @@ import { shouldPassThrough } from '../util/server-utils';
2423import {
2524 getParentSocket ,
2625 buildSocketTimingInfo ,
27- buildSocketEventData ,
26+ buildTlsSocketEventData ,
2827 SocketIsh ,
2928 InitialRemoteAddress ,
3029 InitialRemotePort ,
3130 SocketTimingInfo ,
3231 LastTunnelAddress ,
3332 LastHopEncrypted ,
3433 TlsMetadata ,
35- TlsSetupCompleted
34+ TlsSetupCompleted ,
35+ getAddressAndPort ,
36+ resetOrDestroy
3637} from '../util/socket-util' ;
3738import { MockttpHttpsOptions } from '../mockttp' ;
3839import { buildSocksServer , SocksTcpAddress } from './socks-server' ;
@@ -62,13 +63,6 @@ const originalSocketInit = (<any>tls.TLSSocket.prototype)._init;
6263 } ;
6364} ;
6465
65- export interface ComboServerOptions {
66- debug : boolean ;
67- https : MockttpHttpsOptions | undefined ;
68- http2 : boolean | 'fallback' ;
69- socks : boolean ;
70- } ;
71-
7266// Takes an established TLS socket, calls the error listener if it's silently closed
7367function ifTlsDropped ( socket : tls . TLSSocket , errorCallback : ( ) => void ) {
7468 new Promise ( ( resolve , reject ) => {
@@ -139,26 +133,35 @@ function buildTlsError(
139133 socket : tls . TLSSocket ,
140134 cause : TlsHandshakeFailure [ 'failureCause' ]
141135) : TlsHandshakeFailure {
142- const eventData = buildSocketEventData ( socket ) as TlsHandshakeFailure ;
136+ const eventData = buildTlsSocketEventData ( socket ) as TlsHandshakeFailure ;
143137
144138 eventData . failureCause = cause ;
145139 eventData . timingEvents . failureTimestamp = now ( ) ;
146140
147141 return eventData ;
148142}
149143
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+
150157// The low-level server that handles all the sockets & TLS. The server will correctly call the
151158// given handler for both HTTP & HTTPS direct connections, or connections when used as an
152159// 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 > > {
159161 let server : net . Server ;
160162 let tlsServer : tls . Server | undefined = undefined ;
161163 let socksServer : net . Server | undefined = undefined ;
164+ let unknownProtocolServer : net . Server | undefined = undefined ;
162165
163166 if ( options . https ) {
164167 const ca = await getCA ( options . https ) ;
@@ -217,7 +220,7 @@ export async function createComboServer(
217220 tlsServer ,
218221 options . https . tlsPassthrough ,
219222 options . https . tlsInterceptOnly ,
220- tlsPassthroughListener
223+ options . tlsPassthroughListener
221224 ) ;
222225 }
223226
@@ -243,10 +246,29 @@ export async function createComboServer(
243246 } ) ;
244247 }
245248
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+
246267 server = httpolyglot . createServer ( {
247268 tls : tlsServer ,
248269 socks : socksServer ,
249- } , requestListener ) ;
270+ unknownProtocol : unknownProtocolServer
271+ } , options . requestListener ) ;
250272
251273 // In Node v20, this option was added, rejecting all requests with no host header. While that's good, in
252274 // our case, we want to handle the garbage requests too, so we disable it:
@@ -282,7 +304,7 @@ export async function createComboServer(
282304
283305 socket [ LastHopEncrypted ] = true ;
284306 ifTlsDropped ( socket , ( ) => {
285- tlsClientErrorListener ( socket , buildTlsError ( socket , 'closed' ) ) ;
307+ options . tlsClientErrorListener ( socket , buildTlsError ( socket , 'closed' ) ) ;
286308 } ) ;
287309 } ) ;
288310
@@ -295,7 +317,7 @@ export async function createComboServer(
295317 } ) ;
296318
297319 server . on ( 'tlsClientError' , ( error : Error , socket : tls . TLSSocket ) => {
298- tlsClientErrorListener ( socket , buildTlsError ( socket , getCauseFromError ( error ) ) ) ;
320+ options . tlsClientErrorListener ( socket , buildTlsError ( socket , getCauseFromError ( error ) ) ) ;
299321 } ) ;
300322
301323 // If the server receives a HTTP/HTTPS CONNECT request, Pretend to tunnel, then just re-handle:
@@ -431,30 +453,23 @@ function analyzeAndMaybePassThroughTls(
431453
432454 // SNI is a good clue for where the request is headed, but an explicit proxy address (via
433455 // 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 ;
436458 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 ] ) ) ;
444460 }
445461
446462 socket [ TlsMetadata ] = {
447463 sniHostname,
448- connectHostname,
449- connectPort,
464+ connectHostname : upstreamHostname ,
465+ connectPort : upstreamPort ?. toString ( ) ,
450466 clientAlpn : helloData . alpnProtocols ,
451467 ja3Fingerprint : calculateJa3FromFingerprintData ( helloData . fingerprintData ) ,
452468 ja4Fingerprint : calculateJa4FromHelloData ( helloData )
453469 } ;
454470
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 ) ;
458473 return ; // Do not continue with TLS
459474 } else if ( shouldPassThrough ( sniHostname , passThroughPatterns , interceptOnlyPatterns ) ) {
460475 passthroughListener ( socket , sniHostname ! ) ; // Can't guess the port - not included in SNI
0 commit comments