@@ -9,7 +9,7 @@ import * as streams from 'stream';
9
9
import * as semver from 'semver' ;
10
10
import { makeDestroyable , DestroyableServer } from 'destroyable-server' ;
11
11
import * as httpolyglot from '@httptoolkit/httpolyglot' ;
12
- import { delay } from '@httptoolkit/util' ;
12
+ import { delay , unreachableCheck } from '@httptoolkit/util' ;
13
13
import {
14
14
calculateJa3FromFingerprintData ,
15
15
calculateJa4FromHelloData ,
@@ -27,6 +27,7 @@ import {
27
27
buildSocketEventData
28
28
} from '../util/socket-util' ;
29
29
import { MockttpHttpsOptions } from '../mockttp' ;
30
+ import { buildSocksServer , SocksTcpAddress } from './socks-server' ;
30
31
31
32
// Hardcore monkey-patching: force TLSSocket to link servername & remoteAddress to
32
33
// 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;
53
54
} ;
54
55
} ;
55
56
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 ;
60
62
} ;
61
63
62
64
// Takes an established TLS socket, calls the error listener if it's silently closed
@@ -147,9 +149,10 @@ export async function createComboServer(
147
149
tlsPassthroughListener : ( socket : net . Socket , address : string , port ?: number ) => void
148
150
) : Promise < DestroyableServer < net . Server > > {
149
151
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 ) {
153
156
const ca = await getCA ( options . https ) ;
154
157
const defaultCert = ca . generateCertificate ( options . https . defaultDomain ?? 'localhost' ) ;
155
158
@@ -179,7 +182,7 @@ export async function createComboServer(
179
182
ALPNProtocols : serverProtocolPreferences
180
183
}
181
184
182
- const tlsServer = tls . createServer ( {
185
+ tlsServer = tls . createServer ( {
183
186
key : defaultCert . key ,
184
187
cert : defaultCert . cert ,
185
188
ca : [ defaultCert . ca ] ,
@@ -208,10 +211,35 @@ export async function createComboServer(
208
211
options . https . tlsInterceptOnly ,
209
212
tlsPassthroughListener
210
213
) ;
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 ;
211
232
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
+ } ) ;
213
236
}
214
237
238
+ server = httpolyglot . createServer ( {
239
+ tls : tlsServer ,
240
+ socks : socksServer ,
241
+ } , requestListener ) ;
242
+
215
243
// In Node v20, this option was added, rejecting all requests with no host header. While that's good, in
216
244
// our case, we want to handle the garbage requests too, so we disable it:
217
245
( server as any ) . _httpServer . requireHostHeader = false ;
@@ -393,9 +421,22 @@ function analyzeAndMaybePassThroughTls(
393
421
try {
394
422
const helloData = await readTlsClientHello ( socket ) ;
395
423
396
- const [ connectHostname , connectPort ] = socket . __lastHopConnectAddress ?. split ( ':' ) ?? [ ] ;
397
424
const sniHostname = helloData . serverName ;
398
425
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
+
399
440
socket . __tlsMetadata = {
400
441
sniHostname,
401
442
connectHostname,
0 commit comments