@@ -8,14 +8,11 @@ import type {
8
8
ServerHeartbeatSucceededEvent ,
9
9
TopologyDescription ,
10
10
} from 'mongodb' ;
11
- import type {
12
- ConnectDnsResolutionDetail ,
13
- ConnectEventArgs ,
14
- ConnectEventMap ,
15
- } from './types' ;
11
+ import type { ConnectDnsResolutionDetail } from './types' ;
16
12
import { systemCertsAsync } from 'system-ca' ;
17
13
import type { Options as SystemCAOptions } from 'system-ca' ;
18
14
import type {
15
+ HttpOptions as OIDCHTTPOptions ,
19
16
MongoDBOIDCPlugin ,
20
17
MongoDBOIDCPluginOptions ,
21
18
} from '@mongodb-js/oidc-plugin' ;
@@ -26,7 +23,15 @@ import { StateShareClient, StateShareServer } from './ipc-rpc-state-share';
26
23
import ConnectionString , {
27
24
CommaAndColonSeparatedRecord ,
28
25
} from 'mongodb-connection-string-url' ;
29
- import EventEmitter from 'events' ;
26
+ import { EventEmitter } from 'events' ;
27
+ import {
28
+ createSocks5Tunnel ,
29
+ DevtoolsProxyOptions ,
30
+ AgentWithInitialize ,
31
+ useOrCreateAgent ,
32
+ Tunnel ,
33
+ } from '@mongodb-js/devtools-proxy-support' ;
34
+ export type { DevtoolsProxyOptions , AgentWithInitialize } ;
30
35
31
36
function isAtlas ( str : string ) : boolean {
32
37
try {
@@ -267,6 +272,30 @@ function detectAndLogMissingOptionalDependencies(logger: ConnectLogEmitter) {
267
272
}
268
273
}
269
274
275
+ // Override 'from.emit' so that all events also end up being emitted on 'to'
276
+ function copyEventEmitterEvents < M > (
277
+ from : {
278
+ emit : < K extends string & keyof M > (
279
+ event : K ,
280
+ ...args : M [ K ] extends ( ...args : infer P ) => any ? P : never
281
+ ) => void ;
282
+ } ,
283
+ to : {
284
+ emit : < K extends string & keyof M > (
285
+ event : K ,
286
+ ...args : M [ K ] extends ( ...args : infer P ) => any ? P : never
287
+ ) => void ;
288
+ }
289
+ ) {
290
+ from . emit = function < K extends string & keyof M > (
291
+ event : K ,
292
+ ...args : M [ K ] extends ( ...args : infer P ) => any ? P : never
293
+ ) {
294
+ to . emit ( event , ...args ) ;
295
+ return EventEmitter . prototype . emit . call ( this , event , ...args ) ;
296
+ } ;
297
+ }
298
+
270
299
// Wrapper for all state that a devtools application may want to share
271
300
// between MongoClient instances. Currently, this is only the OIDC state.
272
301
// There are two ways of sharing this state:
@@ -303,13 +332,7 @@ export class DevtoolsConnectionState {
303
332
// (and not other OIDCPlugin instances that might be running on the same logger).
304
333
const proxyingLogger = new EventEmitter ( ) ;
305
334
proxyingLogger . setMaxListeners ( Infinity ) ;
306
- proxyingLogger . emit = function < K extends keyof ConnectEventMap > (
307
- event : K ,
308
- ...args : ConnectEventArgs < K >
309
- ) {
310
- logger . emit ( event , ...args ) ;
311
- return EventEmitter . prototype . emit . call ( this , event , ...args ) ;
312
- } ;
335
+ copyEventEmitterEvents ( proxyingLogger , logger ) ;
313
336
this . oidcPlugin = createMongoDBOIDCPlugin ( {
314
337
...options . oidc ,
315
338
logger : proxyingLogger ,
@@ -318,7 +341,7 @@ export class DevtoolsConnectionState {
318
341
options
319
342
) ,
320
343
...( systemCA
321
- ? addCAToOIDCPluginHttpOptions ( options . oidc , systemCA )
344
+ ? addToOIDCPluginHttpOptions ( options . oidc , { ca : systemCA } )
322
345
: { } ) ,
323
346
} ) ;
324
347
}
@@ -370,6 +393,16 @@ export interface DevtoolsConnectOptions extends MongoClientOptions {
370
393
* extends beyond the lifetime(s) of the respective dependent state instance(s).
371
394
*/
372
395
parentHandle ?: string ;
396
+ /**
397
+ * Proxy options or an existing proxy Agent instance that can be shared. These are applied to
398
+ * both database cluster traffic and, optionally, OIDC HTTP traffic.
399
+ */
400
+ proxy ?: DevtoolsProxyOptions | AgentWithInitialize ;
401
+ /**
402
+ * Whether the proxy specified in `.proxy` should be applied to OIDC HTTP traffic as well.
403
+ * An explicitly specified `agent` in the options for the OIDC plugin will always take precedence.
404
+ */
405
+ applyProxyToOIDC ?: boolean ;
373
406
}
374
407
375
408
/**
@@ -386,82 +419,159 @@ export async function connectMongoClient(
386
419
client : MongoClient ;
387
420
state : DevtoolsConnectionState ;
388
421
} > {
422
+ const cleanupOnClientClose : ( ( ) => void | Promise < void > ) [ ] = [ ] ;
423
+ const runClose = async ( ) => {
424
+ let item : ( ( ) => void | Promise < void > ) | undefined ;
425
+ while ( ( item = cleanupOnClientClose . shift ( ) ) ) await item ( ) ;
426
+ } ;
389
427
detectAndLogMissingOptionalDependencies ( logger ) ;
390
428
391
- let systemCA : string | undefined ;
392
- if ( clientOptions . useSystemCA ) {
393
- const systemCAOpts : SystemCAOptions = { includeNodeCertificates : true } ;
394
- const ca = await systemCertsAsync ( systemCAOpts ) ;
395
- logger . emit ( 'devtools-connect:used-system-ca' , {
396
- caCount : ca . length ,
397
- asyncFallbackError : systemCAOpts . asyncFallbackError ,
398
- } ) ;
399
- systemCA = ca . join ( '\n' ) ;
400
- }
429
+ try {
430
+ let systemCA : string | undefined ;
431
+ // TODO(COMPASS-8077): Remove this option and enable it by default
432
+ if ( clientOptions . useSystemCA ) {
433
+ const systemCAOpts : SystemCAOptions = { includeNodeCertificates : true } ;
434
+ const ca = await systemCertsAsync ( systemCAOpts ) ;
435
+ logger . emit ( 'devtools-connect:used-system-ca' , {
436
+ caCount : ca . length ,
437
+ asyncFallbackError : systemCAOpts . asyncFallbackError ,
438
+ } ) ;
439
+ systemCA = ca . join ( '\n' ) ;
440
+ }
401
441
402
- // If PROVIDER_NAME was specified to the MongoClient options, adding callbacks would conflict
403
- // with that; we should omit them so that e.g. mongosh users can leverage the non-human OIDC
404
- // auth flows by specifying PROVIDER_NAME.
405
- const shouldAddOidcCallbacks = isHumanOidcFlow ( uri , clientOptions ) ;
406
- const state =
407
- clientOptions . parentState ??
408
- new DevtoolsConnectionState ( clientOptions , logger , systemCA ) ;
409
- const mongoClientOptions : MongoClientOptions &
410
- Partial < DevtoolsConnectOptions > = merge (
411
- { } ,
412
- clientOptions ,
413
- shouldAddOidcCallbacks ? state . oidcPlugin . mongoClientOptions : { } ,
414
- systemCA ? { ca : systemCA } : { }
415
- ) ;
442
+ // Create a proxy agent, if requested. `useOrCreateAgent()` takes a target argument
443
+ // that can be used to select a proxy for a specific procotol or host;
444
+ // here we specify 'mongodb://' if we only intend to use the proxy for database
445
+ // connectivity.
446
+ const proxyAgent =
447
+ clientOptions . proxy &&
448
+ useOrCreateAgent (
449
+ 'createConnection' in clientOptions . proxy
450
+ ? clientOptions . proxy
451
+ : {
452
+ ...( clientOptions . proxy as DevtoolsProxyOptions ) ,
453
+ // TODO(COMPASS-8077): Always use explicit CA from either system CA or
454
+ // tlsCAFile option, including one potentially coming from the command line
455
+ ...( systemCA ? { ca : systemCA } : { } ) ,
456
+ } ,
457
+ clientOptions . applyProxyToOIDC ? undefined : 'mongodb://'
458
+ ) ;
459
+ cleanupOnClientClose . push ( ( ) => proxyAgent ?. destroy ( ) ) ;
416
460
417
- // Adopt dns result order changes with Node v18 that affected the VSCode extension VSCODE-458.
418
- // Refs https://github.com/microsoft/vscode/issues/189805
419
- mongoClientOptions . lookup = ( hostname , options , callback ) => {
420
- return dns . lookup ( hostname , { verbatim : false , ...options } , callback ) ;
421
- } ;
461
+ if ( clientOptions . applyProxyToOIDC ) {
462
+ clientOptions . oidc = {
463
+ ...clientOptions . oidc ,
464
+ ...addToOIDCPluginHttpOptions ( clientOptions . oidc , {
465
+ agent : proxyAgent ,
466
+ } ) ,
467
+ } ;
468
+ }
469
+
470
+ let tunnel : Tunnel | undefined ;
471
+ if ( proxyAgent && ! hasProxyHostOption ( uri , clientOptions ) ) {
472
+ tunnel = createSocks5Tunnel ( proxyAgent , 'generate-credentials' ) ;
473
+ cleanupOnClientClose . push ( ( ) => tunnel ?. close ( ) ) ;
474
+ }
475
+ for ( const proxyLogger of new Set ( [ tunnel ?. logger , proxyAgent ?. logger ] ) ) {
476
+ if ( proxyLogger ) {
477
+ copyEventEmitterEvents ( proxyLogger , logger ) ;
478
+ }
479
+ }
480
+ if ( tunnel ) {
481
+ // Should happen after attaching loggers
482
+ await tunnel ?. listen ( ) ;
483
+ clientOptions = {
484
+ ...clientOptions ,
485
+ ...tunnel ?. config ,
486
+ } ;
487
+ }
422
488
423
- delete mongoClientOptions . useSystemCA ;
424
- delete mongoClientOptions . productDocsLink ;
425
- delete mongoClientOptions . productName ;
426
- delete mongoClientOptions . oidc ;
427
- delete mongoClientOptions . parentState ;
428
- delete mongoClientOptions . parentHandle ;
489
+ // If PROVIDER_NAME was specified to the MongoClient options, adding callbacks would conflict
490
+ // with that; we should omit them so that e.g. mongosh users can leverage the non-human OIDC
491
+ // auth flows by specifying PROVIDER_NAME.
492
+ const shouldAddOidcCallbacks = isHumanOidcFlow ( uri , clientOptions ) ;
493
+ const state =
494
+ clientOptions . parentState ??
495
+ new DevtoolsConnectionState ( clientOptions , logger , systemCA ) ;
496
+ const mongoClientOptions : MongoClientOptions &
497
+ Partial < DevtoolsConnectOptions > = merge (
498
+ { } ,
499
+ clientOptions ,
500
+ shouldAddOidcCallbacks ? state . oidcPlugin . mongoClientOptions : { } ,
501
+ systemCA ? { ca : systemCA } : { }
502
+ ) ;
503
+
504
+ // Adopt dns result order changes with Node v18 that affected the VSCode extension VSCODE-458.
505
+ // Refs https://github.com/microsoft/vscode/issues/189805
506
+ mongoClientOptions . lookup = ( hostname , options , callback ) => {
507
+ return dns . lookup ( hostname , { verbatim : false , ...options } , callback ) ;
508
+ } ;
509
+
510
+ delete mongoClientOptions . useSystemCA ;
511
+ delete mongoClientOptions . productDocsLink ;
512
+ delete mongoClientOptions . productName ;
513
+ delete mongoClientOptions . oidc ;
514
+ delete mongoClientOptions . parentState ;
515
+ delete mongoClientOptions . parentHandle ;
516
+ delete mongoClientOptions . proxy ;
517
+ delete mongoClientOptions . applyProxyToOIDC ;
429
518
430
- if (
431
- mongoClientOptions . autoEncryption !== undefined &&
432
- ! mongoClientOptions . autoEncryption . bypassAutoEncryption &&
433
- ! mongoClientOptions . autoEncryption . bypassQueryAnalysis
434
- ) {
435
- // connect first without autoEncryption and serverApi options.
436
- const optionsWithoutFLE = { ...mongoClientOptions } ;
437
- delete optionsWithoutFLE . autoEncryption ;
438
- delete optionsWithoutFLE . serverApi ;
439
- const client = new MongoClientClass ( uri , optionsWithoutFLE ) ;
440
- closeMongoClientWhenAuthFails ( state , client ) ;
441
- await connectWithFailFast ( uri , client , logger ) ;
442
- const buildInfo = await client
443
- . db ( 'admin' )
444
- . admin ( )
445
- . command ( { buildInfo : 1 } ) ;
446
- await client . close ( ) ;
447
519
if (
448
- ! buildInfo . modules ?. includes ( 'enterprise' ) &&
449
- ! buildInfo . gitVersion ?. match ( / e n t e r p r i s e / )
520
+ mongoClientOptions . autoEncryption !== undefined &&
521
+ ! mongoClientOptions . autoEncryption . bypassAutoEncryption &&
522
+ ! mongoClientOptions . autoEncryption . bypassQueryAnalysis
450
523
) {
451
- throw new MongoAutoencryptionUnavailable ( ) ;
524
+ // connect first without autoEncryption and serverApi options.
525
+ const optionsWithoutFLE = { ...mongoClientOptions } ;
526
+ delete optionsWithoutFLE . autoEncryption ;
527
+ delete optionsWithoutFLE . serverApi ;
528
+ const client = new MongoClientClass ( uri , optionsWithoutFLE ) ;
529
+ closeMongoClientWhenAuthFails ( state , client ) ;
530
+ await connectWithFailFast ( uri , client , logger ) ;
531
+ const buildInfo = await client
532
+ . db ( 'admin' )
533
+ . admin ( )
534
+ . command ( { buildInfo : 1 } ) ;
535
+ await client . close ( ) ;
536
+ if (
537
+ ! buildInfo . modules ?. includes ( 'enterprise' ) &&
538
+ ! buildInfo . gitVersion ?. match ( / e n t e r p r i s e / )
539
+ ) {
540
+ throw new MongoAutoencryptionUnavailable ( ) ;
541
+ }
542
+ }
543
+ uri = await resolveMongodbSrv ( uri , logger ) ;
544
+ const client = new MongoClientClass ( uri , mongoClientOptions ) ;
545
+ client . once ( 'close' , runClose ) ;
546
+ closeMongoClientWhenAuthFails ( state , client ) ;
547
+ await connectWithFailFast ( uri , client , logger ) ;
548
+ if ( ( client as any ) . autoEncrypter ) {
549
+ // Enable Devtools-specific CSFLE result decoration.
550
+ ( client as any ) . autoEncrypter [
551
+ Symbol . for ( '@@mdb.decorateDecryptionResult' )
552
+ ] = true ;
452
553
}
554
+ return { client, state } ;
555
+ } catch ( err : unknown ) {
556
+ await runClose ( ) ;
557
+ throw err ;
453
558
}
454
- uri = await resolveMongodbSrv ( uri , logger ) ;
455
- const client = new MongoClientClass ( uri , mongoClientOptions ) ;
456
- closeMongoClientWhenAuthFails ( state , client ) ;
457
- await connectWithFailFast ( uri , client , logger ) ;
458
- if ( ( client as any ) . autoEncrypter ) {
459
- // Enable Devtools-specific CSFLE result decoration.
460
- ( client as any ) . autoEncrypter [
461
- Symbol . for ( '@@mdb.decorateDecryptionResult' )
462
- ] = true ;
559
+ }
560
+
561
+ function hasProxyHostOption (
562
+ uri : string ,
563
+ clientOptions : MongoClientOptions
564
+ ) : boolean {
565
+ if ( clientOptions . proxyHost || clientOptions . proxyPort ) return true ;
566
+ let cs : ConnectionString ;
567
+ try {
568
+ cs = new ConnectionString ( uri , { looseValidation : true } ) ;
569
+ } catch {
570
+ return false ;
463
571
}
464
- return { client, state } ;
572
+
573
+ const sp = cs . typedSearchParams < MongoClientOptions > ( ) ;
574
+ return sp . has ( 'proxyHost' ) || sp . has ( 'proxyPort' ) ;
465
575
}
466
576
467
577
export function isHumanOidcFlow (
@@ -530,16 +640,20 @@ function closeMongoClientWhenAuthFails(
530
640
) ;
531
641
}
532
642
533
- function addCAToOIDCPluginHttpOptions (
643
+ function addToOIDCPluginHttpOptions (
534
644
existingOIDCPluginOptions : MongoDBOIDCPluginOptions | undefined ,
535
- ca : string
645
+ addedOptions : Partial < OIDCHTTPOptions >
536
646
) : Pick < MongoDBOIDCPluginOptions , 'customHttpOptions' > {
537
647
const existingCustomOptions = existingOIDCPluginOptions ?. customHttpOptions ;
538
648
if ( typeof existingCustomOptions === 'function' ) {
539
649
return {
540
650
customHttpOptions : ( url , options , ...restArgs ) =>
541
- existingCustomOptions ( url , { ...options , ca } , ...restArgs ) ,
651
+ existingCustomOptions (
652
+ url ,
653
+ { ...options , ...addedOptions } ,
654
+ ...restArgs
655
+ ) ,
542
656
} ;
543
657
}
544
- return { customHttpOptions : { ...existingCustomOptions , ca } } ;
658
+ return { customHttpOptions : { ...existingCustomOptions , ... addedOptions } } ;
545
659
}
0 commit comments