1+ import { promisify } from 'util' ;
12import { URL , URLSearchParams } from 'whatwg-url' ;
23
34class MongoParseError extends Error { }
45
6+ type Address = { name : string ; port : number } ;
57type Options = {
68 dns ?: {
7- resolveSrv ( hostname : string , cb : ( err : Error | undefined | null , addresses : { name : string , port : number } [ ] | undefined ) => void ) : void ;
9+ resolveSrv ( hostname : string , cb : ( err : Error | undefined | null , addresses : Address [ ] | undefined ) => void ) : void ;
810 resolveTxt ( hostname : string , cb : ( err : Error | undefined | null , addresses : string [ ] [ ] | undefined ) => void ) : void ;
911 } ;
1012} ;
1113
14+ const ALLOWED_TXT_OPTIONS : Readonly < string [ ] > = [ 'authSource' , 'replicaSet' , 'loadBalanced' ] ;
15+
1216function matchesParentDomain ( srvAddress : string , parentDomain : string ) : boolean {
1317 const regex = / ^ .* ?\. / ;
1418 const srv = `.${ srvAddress . replace ( regex , '' ) } ` ;
1519 const parent = `.${ parentDomain . replace ( regex , '' ) } ` ;
1620 return srv . endsWith ( parent ) ;
1721}
1822
23+ async function resolveDnsSrvRecord ( dns : NonNullable < Options [ 'dns' ] > , lookupAddress : string ) : Promise < string [ ] > {
24+ const addresses = await promisify ( dns . resolveSrv ) ( `_mongodb._tcp.${ lookupAddress } ` ) ;
25+ if ( ! addresses ?. length ) {
26+ throw new MongoParseError ( 'No addresses found at host' ) ;
27+ }
28+
29+ for ( const { name } of addresses ) {
30+ if ( ! matchesParentDomain ( name , lookupAddress ) ) {
31+ throw new MongoParseError ( 'Server record does not share hostname with parent URI' ) ;
32+ }
33+ }
34+
35+ return addresses . map ( r => r . name + ( ( r . port ?? 27017 ) === 27017 ? '' : `:${ r . port } ` ) ) ;
36+ }
37+
38+ async function resolveDnsTxtRecord ( dns : NonNullable < Options [ 'dns' ] > , lookupAddress : string ) : Promise < URLSearchParams > {
39+ let records : string [ ] [ ] | undefined ;
40+ try {
41+ records = await promisify ( dns . resolveTxt ) ( lookupAddress ) ;
42+ } catch ( err ) {
43+ if ( err . code && ( err . code !== 'ENODATA' && err . code !== 'ENOTFOUND' ) ) {
44+ throw err ;
45+ }
46+ }
47+
48+ let txtRecord : string ;
49+ if ( records && records . length > 1 ) {
50+ throw new MongoParseError ( 'Multiple text records not allowed' ) ;
51+ } else {
52+ txtRecord = records ?. [ 0 ] ?. join ( '' ) ?? '' ;
53+ }
54+
55+ const txtRecordOptions = new URLSearchParams ( txtRecord ) ;
56+ const txtRecordOptionKeys = [ ...txtRecordOptions . keys ( ) ] ;
57+ if ( txtRecordOptionKeys . some ( key => ! ALLOWED_TXT_OPTIONS . includes ( key ) ) ) {
58+ throw new MongoParseError ( `Text record must only set ${ ALLOWED_TXT_OPTIONS . join ( ', ' ) } ` ) ;
59+ }
60+
61+ const source = txtRecordOptions . get ( 'authSource' ) ?? undefined ;
62+ const replicaSet = txtRecordOptions . get ( 'replicaSet' ) ?? undefined ;
63+ const loadBalanced = txtRecordOptions . get ( 'loadBalanced' ) ?? undefined ;
64+
65+ if ( source === '' || replicaSet === '' || loadBalanced === '' ) {
66+ throw new MongoParseError ( 'Cannot have empty URI params in DNS TXT Record' ) ;
67+ }
68+
69+ if ( loadBalanced !== undefined && loadBalanced !== 'true' && loadBalanced !== 'false' ) {
70+ throw new MongoParseError ( `DNS TXT Record contains invalid value ${ loadBalanced } for loadBalanced option (allowed: true, false)` ) ;
71+ }
72+
73+ return txtRecordOptions ;
74+ }
75+
1976async function resolveMongodbSrv ( input : string , options ?: Options ) : Promise < string > {
2077 const dns = options ?. dns ?? require ( 'dns' ) ;
2178
@@ -33,60 +90,8 @@ async function resolveMongodbSrv (input: string, options?: Options): Promise<str
3390
3491 const lookupAddress = url . hostname ;
3592 const [ srvResult , txtResult ] = await Promise . all ( [
36- ( async ( ) => {
37- const addresses = await new Promise < { name : string , port : number } [ ] > ( ( resolve , reject ) => {
38- dns . resolveSrv ( `_mongodb._tcp.${ lookupAddress } ` ,
39- ( err : Error | null , addresses : { name : string , port : number } [ ] ) => {
40- if ( err ) return reject ( err ) ;
41- return resolve ( addresses ) ;
42- } ) ;
43- } ) ;
44- if ( addresses . length === 0 ) {
45- throw new MongoParseError ( 'No addresses found at host' ) ;
46- }
47-
48- for ( const { name } of addresses ) {
49- if ( ! matchesParentDomain ( name , lookupAddress ) ) {
50- throw new MongoParseError ( 'Server record does not share hostname with parent URI' ) ;
51- }
52- }
53-
54- return addresses . map ( r => r . name + ( ( r . port ?? 27017 ) === 27017 ? '' : `:${ r . port } ` ) ) ;
55- } ) ( ) ,
56- ( async ( ) => {
57- const txtRecord = await new Promise < string > ( ( resolve , reject ) => {
58- dns . resolveTxt ( lookupAddress , ( err : ( Error & { code : string } ) | null , record : string [ ] [ ] ) => {
59- if ( err ) {
60- if ( err . code && ( err . code !== 'ENODATA' && err . code !== 'ENOTFOUND' ) ) {
61- reject ( err ) ;
62- } else {
63- resolve ( '' ) ;
64- }
65- } else {
66- if ( record . length > 1 ) {
67- reject ( new MongoParseError ( 'Multiple text records not allowed' ) ) ;
68- } else {
69- resolve ( record [ 0 ] ?. join ( '' ) ?? '' ) ;
70- }
71- }
72- } ) ;
73- } ) ;
74-
75- const txtRecordOptions = new URLSearchParams ( txtRecord ) ;
76- const txtRecordOptionKeys = [ ...txtRecordOptions . keys ( ) ] ;
77- if ( txtRecordOptionKeys . some ( key => key !== 'authSource' && key !== 'replicaSet' ) ) {
78- throw new MongoParseError ( 'Text record must only set `authSource` or `replicaSet`' ) ;
79- }
80-
81- const source = txtRecordOptions . get ( 'authSource' ) ?? undefined ;
82- const replicaSet = txtRecordOptions . get ( 'replicaSet' ) ?? undefined ;
83-
84- if ( source === '' || replicaSet === '' ) {
85- throw new MongoParseError ( 'Cannot have empty URI params in DNS TXT Record' ) ;
86- }
87-
88- return txtRecordOptions ;
89- } ) ( )
93+ resolveDnsSrvRecord ( dns , lookupAddress ) ,
94+ resolveDnsTxtRecord ( dns , lookupAddress )
9095 ] ) ;
9196
9297 url . protocol = 'mongodb:' ;
0 commit comments