@@ -9,7 +9,7 @@ import * as streams from 'stream';
99import * as semver from 'semver' ;
1010import { makeDestroyable , DestroyableServer } from 'destroyable-server' ;
1111import * as httpolyglot from '@httptoolkit/httpolyglot' ;
12- import { delay } from '@httptoolkit/util' ;
12+ import { delay , unreachableCheck } from '@httptoolkit/util' ;
1313import {
1414 calculateJa3FromFingerprintData ,
1515 calculateJa4FromHelloData ,
@@ -27,6 +27,7 @@ import {
2727 buildSocketEventData
2828} from '../util/socket-util' ;
2929import { MockttpHttpsOptions } from '../mockttp' ;
30+ import { buildSocksServer , SocksTcpAddress } from './socks-server' ;
3031
3132// Hardcore monkey-patching: force TLSSocket to link servername & remoteAddress to
3233// sockets as soon as they're available, without waiting for the handshake to fully
@@ -53,10 +54,11 @@ const originalSocketInit = (<any>tls.TLSSocket.prototype)._init;
5354 } ;
5455} ;
5556
56- export type ComboServerOptions = {
57- debug : boolean ,
58- https : MockttpHttpsOptions | undefined ,
59- http2 : true | false | 'fallback'
57+ export interface ComboServerOptions {
58+ debug : boolean ;
59+ https : MockttpHttpsOptions | undefined ;
60+ http2 : boolean | 'fallback' ;
61+ socks : boolean ;
6062} ;
6163
6264// Takes an established TLS socket, calls the error listener if it's silently closed
@@ -147,9 +149,10 @@ export async function createComboServer(
147149 tlsPassthroughListener : ( socket : net . Socket , address : string , port ?: number ) => void
148150) : Promise < DestroyableServer < net . Server > > {
149151 let server : net . Server ;
150- if ( ! options . https ) {
151- server = httpolyglot . createServer ( requestListener ) ;
152- } else {
152+ let tlsServer : tls . Server | undefined = undefined ;
153+ let socksServer : net . Server | undefined = undefined ;
154+
155+ if ( options . https ) {
153156 const ca = await getCA ( options . https ) ;
154157 const defaultCert = ca . generateCertificate ( options . https . defaultDomain ?? 'localhost' ) ;
155158
@@ -179,7 +182,7 @@ export async function createComboServer(
179182 ALPNProtocols : serverProtocolPreferences
180183 }
181184
182- const tlsServer = tls . createServer ( {
185+ tlsServer = tls . createServer ( {
183186 key : defaultCert . key ,
184187 cert : defaultCert . cert ,
185188 ca : [ defaultCert . ca ] ,
@@ -208,10 +211,35 @@ export async function createComboServer(
208211 options . https . tlsInterceptOnly ,
209212 tlsPassthroughListener
210213 ) ;
214+ }
215+
216+ if ( options . socks ) {
217+ socksServer = buildSocksServer ( ) ;
218+ socksServer . on ( 'socks-tcp-connect' , ( socket : net . Socket , address : SocksTcpAddress ) => {
219+ const addressString =
220+ address . type === 'ipv4'
221+ ? `${ address . ip } :${ address . port } `
222+ : address . type === 'ipv6'
223+ ? `[${ address . ip } ]:${ address . port } `
224+ : address . type === 'hostname'
225+ ? `${ address . hostname } :${ address . port } `
226+ : unreachableCheck ( address )
227+
228+ if ( options . debug ) console . log ( `Proxying SOCKS TCP connection to ${ addressString } ` ) ;
229+
230+ socket . __timingInfo ! . tunnelSetupTimestamp = now ( ) ;
231+ socket . __lastHopConnectAddress = addressString ;
211232
212- server = httpolyglot . createServer ( tlsServer , requestListener ) ;
233+ // Put the socket back into the server, so we can handle the data within:
234+ server . emit ( 'connection' , socket ) ;
235+ } ) ;
213236 }
214237
238+ server = httpolyglot . createServer ( {
239+ tls : tlsServer ,
240+ socks : socksServer ,
241+ } , requestListener ) ;
242+
215243 // In Node v20, this option was added, rejecting all requests with no host header. While that's good, in
216244 // our case, we want to handle the garbage requests too, so we disable it:
217245 ( server as any ) . _httpServer . requireHostHeader = false ;
@@ -393,9 +421,22 @@ function analyzeAndMaybePassThroughTls(
393421 try {
394422 const helloData = await readTlsClientHello ( socket ) ;
395423
396- const [ connectHostname , connectPort ] = socket . __lastHopConnectAddress ?. split ( ':' ) ?? [ ] ;
397424 const sniHostname = helloData . serverName ;
398425
426+ // SNI is a good clue for where the request is headed, but an explicit proxy address (via
427+ // CONNECT or SOCKS) is even better. Note that this may be a hostname or IPv4/6 address:
428+ let connectHostname : string | undefined ;
429+ let connectPort : string | undefined ;
430+ if ( socket . __lastHopConnectAddress ) {
431+ const lastColonIndex = socket . __lastHopConnectAddress . lastIndexOf ( ':' ) ;
432+ if ( lastColonIndex !== - 1 ) {
433+ connectHostname = socket . __lastHopConnectAddress . slice ( 0 , lastColonIndex ) ;
434+ connectPort = socket . __lastHopConnectAddress . slice ( lastColonIndex + 1 ) ;
435+ } else {
436+ connectHostname = socket . __lastHopConnectAddress ;
437+ }
438+ }
439+
399440 socket . __tlsMetadata = {
400441 sniHostname,
401442 connectHostname,
0 commit comments