@@ -2,6 +2,7 @@ import * as url from 'url';
2
2
import * as qs from 'querystring' ;
3
3
import * as dns from 'dns' ;
4
4
import { URL } from 'url' ;
5
+ import { AuthMechanismEnum } from './cmap/auth/defaultAuthProviders' ;
5
6
import { ReadPreference , ReadPreferenceMode } from './read_preference' ;
6
7
import { ReadConcern , ReadConcernLevel } from './read_concern' ;
7
8
import { W , WriteConcern } from './write_concern' ;
@@ -13,7 +14,8 @@ import type { CompressorName } from './cmap/wire_protocol/compression';
13
14
import type { DriverInfo , MongoClientOptions , MongoOptions , PkFactory } from './mongo_client' ;
14
15
import { MongoCredentials } from './cmap/auth/mongo_credentials' ;
15
16
import type { TagSet } from './sdam/server_description' ;
16
- import { Logger , LoggerLevel } from '.' ;
17
+ import { Logger , LoggerLevel } from './logger' ;
18
+ import { ObjectId } from 'bson' ;
17
19
18
20
/**
19
21
* The following regular expression validates a connection string and breaks the
@@ -506,23 +508,6 @@ function checkTLSQueryString(queryString: any) {
506
508
}
507
509
}
508
510
509
- /**
510
- * Checks options object if both options are present (any value) will throw an error.
511
- *
512
- * @param options - The options used for options parsing
513
- * @param optionKeyA - A options key
514
- * @param optionKeyB - B options key
515
- * @throws MongoParseError if two provided options are mutually exclusive.
516
- */
517
- function assertRepelOptions ( options : AnyOptions , optionKeyA : string , optionKeyB : string ) {
518
- if (
519
- Object . prototype . hasOwnProperty . call ( options , optionKeyA ) &&
520
- Object . prototype . hasOwnProperty . call ( options , optionKeyB )
521
- ) {
522
- throw new MongoParseError ( `The \`${ optionKeyA } \` option cannot be used with \`${ optionKeyB } \`` ) ;
523
- }
524
- }
525
-
526
511
/**
527
512
* Checks if TLS options are valid
528
513
*
@@ -775,7 +760,7 @@ export function parseConnectionString(
775
760
// NEW PARSER WORK...
776
761
777
762
const HOSTS_REGEX = new RegExp (
778
- ' (?<protocol>mongodb(?:\\ +srv|)):\\/\\ /(?:(?<username>[^:]*)(?::(?<password>[^@]*))?@)?(?<hosts>[^\\/?]* )(?<rest>.*)'
763
+ String . raw ` (?<protocol>mongodb(?:\+srv|)):\/\ /(?:(?<username>[^:]*)(?::(?<password>[^@]*))?@)?(?<hosts>(?!:) [^\/?@]+ )(?<rest>.*)`
779
764
) ;
780
765
781
766
function parseURI ( uri : string ) : { srv : boolean ; url : URL ; hosts : string [ ] } {
@@ -794,10 +779,39 @@ function parseURI(uri: string): { srv: boolean; url: URL; hosts: string[] } {
794
779
throw new MongoParseError ( 'Invalid connection string, protocol and host(s) required' ) ;
795
780
}
796
781
782
+ decodeURIComponent ( username ?? '' ) ;
783
+ decodeURIComponent ( password ?? '' ) ;
784
+
785
+ const illegalCharacters = new RegExp ( '[@/:]' , 'gi' ) ;
786
+ if ( username ?. match ( illegalCharacters ) ) {
787
+ throw new MongoParseError ( `Username contains unescaped characters ${ username } ` ) ;
788
+ }
789
+ if ( ! username || ! password ) {
790
+ const uriWithoutProtocol = uri . replace ( `${ protocol } ://` , '' ) ;
791
+ if ( uriWithoutProtocol . startsWith ( '@' ) || uriWithoutProtocol . startsWith ( ':' ) ) {
792
+ throw new MongoParseError ( 'URI contained empty userinfo section' ) ;
793
+ }
794
+ }
795
+
796
+ if ( password ?. match ( illegalCharacters ) ) {
797
+ throw new MongoParseError ( 'Password contains unescaped characters' ) ;
798
+ }
799
+
797
800
const authString = username ? ( password ? `${ username } :${ password } ` : `${ username } ` ) : '' ;
801
+ const srv = protocol . includes ( 'srv' ) ;
802
+ const hostList = hosts . split ( ',' ) ;
803
+ const url = new URL ( `${ protocol . toLowerCase ( ) } ://${ authString } @dummyHostname${ rest } ` ) ;
804
+
805
+ if ( srv && hostList . length !== 1 ) {
806
+ throw new MongoParseError ( 'mongodb+srv URI cannot have multiple service names' ) ;
807
+ }
808
+ if ( srv && hostList [ 0 ] . includes ( ':' ) ) {
809
+ throw new MongoParseError ( 'mongodb+srv URI cannot have port number' ) ;
810
+ }
811
+
798
812
return {
799
- srv : protocol . includes ( 'srv' ) ,
800
- url : new URL ( ` ${ protocol . toLowerCase ( ) } :// ${ authString } @dummyHostname ${ rest } ` ) ,
813
+ srv,
814
+ url,
801
815
hosts : hosts . split ( ',' )
802
816
} ;
803
817
}
@@ -841,12 +855,60 @@ function toRecord(value: string): Record<string, any> {
841
855
return record ;
842
856
}
843
857
844
- const defaultOptions = new Map < string , unknown > ( [
845
- [ 'dbName' , 'test' ] ,
846
- [ 'socketTimeoutMS' , 0 ] ,
847
- [ 'readPreference' , ReadPreference . primary ]
848
- // TODO: add more
849
- ] ) ;
858
+ const defaultOptions = new Map < string , unknown > (
859
+ ( [
860
+ [ 'readPreference' , ReadPreference . primary ] ,
861
+ [ 'autoEncryption' , { } ] ,
862
+ [ 'compression' , 'none' ] ,
863
+ [ 'compressors' , 'none' ] ,
864
+ [ 'connectTimeoutMS' , 30000 ] ,
865
+ [ 'dbName' , 'test' ] ,
866
+ [ 'directConnection' , false ] ,
867
+ [ 'driverInfo' , { } ] ,
868
+ [ 'forceServerObjectId' , false ] ,
869
+ [ 'gssapiServiceName' , undefined ] ,
870
+ [ 'heartbeatFrequencyMS' , 10000 ] ,
871
+ [ 'keepAlive' , true ] ,
872
+ [ 'keepAliveInitialDelay' , 120000 ] ,
873
+ [ 'localThresholdMS' , 0 ] ,
874
+ [ 'logger' , new Logger ( 'MongoClient' ) ] ,
875
+ [ 'maxIdleTimeMS' , 0 ] ,
876
+ [ 'maxPoolSize' , 100 ] ,
877
+ [ 'minPoolSize' , 0 ] ,
878
+ [ 'monitorCommands' , false ] ,
879
+ [ 'noDelay' , true ] ,
880
+ [ 'numberOfRetries' , 5 ] ,
881
+ [
882
+ 'pkFactory' ,
883
+ {
884
+ createPk ( ) {
885
+ // We prefer not to rely on ObjectId having a createPk method
886
+ return new ObjectId ( ) ;
887
+ }
888
+ }
889
+ ] ,
890
+ [
891
+ 'createPk' ,
892
+ function createPk ( ) {
893
+ // We prefer not to rely on ObjectId having a createPk method
894
+ return new ObjectId ( ) ;
895
+ }
896
+ ] ,
897
+ [ 'promiseLibrary' , undefined ] ,
898
+ [ 'raw' , false ] ,
899
+ [ 'reconnectInterval' , 0 ] ,
900
+ [ 'reconnectTries' , 0 ] ,
901
+ [ 'replicaSet' , undefined ] ,
902
+ [ 'retryReads' , true ] ,
903
+ [ 'retryWrites' , true ] ,
904
+ [ 'serverSelectionTimeoutMS' , 30000 ] ,
905
+ [ 'serverSelectionTryOnce' , false ] ,
906
+ [ 'socketTimeoutMS' , 0 ] ,
907
+ [ 'waitQueueMultiple' , 1 ] ,
908
+ [ 'waitQueueTimeoutMS' , 0 ] ,
909
+ [ 'zlibCompressionLevel' , 0 ]
910
+ ] as [ string , any ] [ ] ) . map ( ( [ k , v ] ) => [ k . toLowerCase ( ) , v ] )
911
+ ) ;
850
912
851
913
export function parseOptions (
852
914
uri : string ,
@@ -855,16 +917,29 @@ export function parseOptions(
855
917
const { url, hosts, srv } = parseURI ( uri ) ;
856
918
857
919
const mongoOptions = Object . create ( null ) ;
858
- mongoOptions . hosts = hosts ;
920
+ mongoOptions . hosts = srv ? [ { host : hosts [ 0 ] , type : 'srv' } ] : hosts . map ( toHostArray ) ;
859
921
mongoOptions . srv = srv ;
922
+ mongoOptions . dbName = decodeURIComponent (
923
+ url . pathname [ 0 ] === '/' ? url . pathname . slice ( 1 ) : url . pathname
924
+ ) ;
860
925
861
926
const urlOptions = new Map ( ) ;
927
+
928
+ if ( url . username ) urlOptions . set ( 'username' , [ url . username ] ) ;
929
+ if ( url . password ) urlOptions . set ( 'password' , [ url . password ] ) ;
930
+
862
931
for ( const key of url . searchParams . keys ( ) ) {
863
932
const loweredKey = key . toLowerCase ( ) ;
933
+ const values = [ ...url . searchParams . getAll ( key ) ] ;
934
+
935
+ if ( values . includes ( '' ) ) {
936
+ throw new MongoParseError ( 'URI cannot contain options with no value' ) ;
937
+ }
938
+
864
939
if ( urlOptions . has ( loweredKey ) ) {
865
- urlOptions . set ( loweredKey , [ ... urlOptions . get ( loweredKey ) , ... url . searchParams . getAll ( key ) ] ) ;
940
+ urlOptions . get ( loweredKey ) ?. push ( ... values ) ;
866
941
} else {
867
- urlOptions . set ( loweredKey , url . searchParams . getAll ( key ) ) ;
942
+ urlOptions . set ( loweredKey , values ) ;
868
943
}
869
944
}
870
945
@@ -895,51 +970,88 @@ export function parseOptions(
895
970
}
896
971
897
972
for ( const [ loweredKey , values ] of allOptions . entries ( ) ) {
898
- const descriptor = descriptorFor ( loweredKey ) ;
899
- const {
900
- descriptor : { rename, type, transform, deprecated } ,
901
- key
902
- } = descriptor ;
903
- const name = rename ?? key ;
904
-
905
- if ( deprecated ) {
906
- console . warn ( `${ key } is a deprecated option` ) ;
907
- }
973
+ setOption ( mongoOptions , loweredKey , values ) ;
974
+ }
908
975
909
- switch ( type ) {
910
- case 'boolean' :
911
- mongoOptions [ name ] = getBoolean ( name , values [ 0 ] ) ;
912
- break ;
913
- case 'int' :
914
- mongoOptions [ name ] = getInt ( name , values [ 0 ] ) ;
915
- break ;
916
- case 'uint' :
917
- mongoOptions [ name ] = getUint ( name , values [ 0 ] ) ;
918
- break ;
919
- case 'string' :
920
- mongoOptions [ name ] = String ( values [ 0 ] ) ;
921
- break ;
922
- case 'record' :
923
- if ( ! isRecord ( values [ 0 ] ) ) {
924
- throw new TypeError ( `${ name } must be an object` ) ;
925
- }
926
- mongoOptions [ name ] = values [ 0 ] ;
927
- break ;
928
- case 'asIs' :
929
- mongoOptions [ name ] = values [ 0 ] ;
930
- break ;
931
- default : {
932
- if ( ! transform ) {
933
- throw new MongoParseError ( 'Descriptors missing a type must define a transform' ) ;
934
- }
935
- const transformValue = transform ( { name, options : mongoOptions , values } ) ;
936
- mongoOptions [ name ] = transformValue ;
976
+ mongoOptions . credentials ?. validate ( ) ;
977
+ checkTLSOptions ( mongoOptions ) ;
978
+
979
+ return Object . freeze ( mongoOptions ) as Readonly < MongoOptions > ;
980
+ }
981
+
982
+ function setOption ( mongoOptions : any , loweredKey : string , values : unknown [ ] ) {
983
+ const descriptor = descriptorFor ( loweredKey ) ;
984
+ const {
985
+ descriptor : { rename, type, transform, deprecated } ,
986
+ key
987
+ } = descriptor ;
988
+ const name = rename ?? key ;
989
+
990
+ if ( deprecated ) {
991
+ console . warn ( `${ key } is a deprecated option` ) ;
992
+ }
993
+
994
+ switch ( type ) {
995
+ case 'boolean' :
996
+ mongoOptions [ name ] = getBoolean ( name , values [ 0 ] ) ;
997
+ break ;
998
+ case 'int' :
999
+ mongoOptions [ name ] = getInt ( name , values [ 0 ] ) ;
1000
+ break ;
1001
+ case 'uint' :
1002
+ mongoOptions [ name ] = getUint ( name , values [ 0 ] ) ;
1003
+ break ;
1004
+ case 'string' :
1005
+ if ( values [ 0 ] === undefined ) {
937
1006
break ;
938
1007
}
1008
+ mongoOptions [ name ] = String ( values [ 0 ] ) ;
1009
+ break ;
1010
+ case 'record' :
1011
+ if ( ! isRecord ( values [ 0 ] ) ) {
1012
+ throw new TypeError ( `${ name } must be an object` ) ;
1013
+ }
1014
+ mongoOptions [ name ] = values [ 0 ] ;
1015
+ break ;
1016
+ case 'asIs' :
1017
+ mongoOptions [ name ] = values [ 0 ] ;
1018
+ break ;
1019
+ default : {
1020
+ if ( ! transform ) {
1021
+ throw new MongoParseError ( 'Descriptors missing a type must define a transform' ) ;
1022
+ }
1023
+ const transformValue = transform ( { name, options : mongoOptions , values } ) ;
1024
+ mongoOptions [ name ] = transformValue ;
1025
+ break ;
939
1026
}
940
1027
}
1028
+ }
941
1029
942
- return Object . freeze ( mongoOptions ) as Readonly < MongoOptions > ;
1030
+ function toHostArray ( hostString : string ) {
1031
+ const parsedHost = new URL ( `mongodb://${ hostString . split ( ' ' ) . join ( '%20' ) } ` ) ;
1032
+
1033
+ let socketPath ;
1034
+ if ( hostString . match ( / \. s o c k / ) ) {
1035
+ // heuristically determine if we're working with a domain socket
1036
+ socketPath = decodeURIComponent ( hostString ) ;
1037
+ }
1038
+
1039
+ const result : Host = socketPath
1040
+ ? {
1041
+ host : socketPath ,
1042
+ type : 'unix'
1043
+ }
1044
+ : {
1045
+ host : parsedHost . hostname ,
1046
+ port : parsedHost . port ? parseInt ( parsedHost . port ) : 27017 ,
1047
+ type : 'tcp'
1048
+ } ;
1049
+
1050
+ if ( result . type === 'tcp' && result . port === 0 ) {
1051
+ throw new MongoParseError ( 'Invalid port (zero) with hostname' ) ;
1052
+ }
1053
+
1054
+ return result ;
943
1055
}
944
1056
945
1057
interface OptionDescriptor {
@@ -983,7 +1095,16 @@ export const OPTIONS: Record<keyof MongoClientOptions, OptionDescriptor> = {
983
1095
if ( ! mechanism ) {
984
1096
throw new TypeError ( `authMechanism one of ${ mechanisms } , got ${ value } ` ) ;
985
1097
}
986
- return new MongoCredentials ( { ...options . credentials , mechanism } ) ;
1098
+ let db ; // some mechanisms have '$external' as the Auth Source
1099
+ if (
1100
+ mechanism === 'PLAIN' ||
1101
+ mechanism === 'GSSAPI' ||
1102
+ mechanism === 'MONGODB-AWS' ||
1103
+ mechanism === 'MONGODB-X509'
1104
+ ) {
1105
+ db = '$external' ;
1106
+ }
1107
+ return new MongoCredentials ( { ...options . credentials , mechanism, db } ) ;
987
1108
}
988
1109
} ,
989
1110
authMechanismProperties : {
@@ -1181,9 +1302,8 @@ export const OPTIONS: Record<keyof MongoClientOptions, OptionDescriptor> = {
1181
1302
minPoolSize : {
1182
1303
type : 'uint'
1183
1304
} ,
1184
- minSize : {
1185
- type : 'uint' ,
1186
- rename : 'minPoolSize'
1305
+ minHeartbeatFrequencyMS : {
1306
+ type : 'uint'
1187
1307
} ,
1188
1308
monitorCommands : {
1189
1309
type : 'boolean'
@@ -1209,6 +1329,15 @@ export const OPTIONS: Record<keyof MongoClientOptions, OptionDescriptor> = {
1209
1329
return new MongoCredentials ( { ...options . credentials , password } ) ;
1210
1330
}
1211
1331
} as OptionDescriptor ,
1332
+ password : {
1333
+ rename : 'credentials' ,
1334
+ transform ( { values : [ password ] , options } ) {
1335
+ if ( typeof password !== 'string' ) {
1336
+ throw new TypeError ( 'pass must be a string' ) ;
1337
+ }
1338
+ return new MongoCredentials ( { ...options . credentials , password } ) ;
1339
+ }
1340
+ } ,
1212
1341
pkFactory : {
1213
1342
rename : 'createPk' ,
1214
1343
transform ( { values : [ value ] } ) : PkFactory {
@@ -1226,10 +1355,6 @@ export const OPTIONS: Record<keyof MongoClientOptions, OptionDescriptor> = {
1226
1355
return { ...options . driverInfo , platform : String ( value ) } ;
1227
1356
}
1228
1357
} as OptionDescriptor ,
1229
- poolSize : {
1230
- rename : 'maxPoolSize' ,
1231
- type : 'uint'
1232
- } ,
1233
1358
promiseLibrary : {
1234
1359
type : 'asIs'
1235
1360
} ,
@@ -1395,10 +1520,17 @@ export const OPTIONS: Record<keyof MongoClientOptions, OptionDescriptor> = {
1395
1520
type : 'boolean'
1396
1521
} ,
1397
1522
user : {
1523
+ rename : 'credentials' ,
1398
1524
transform ( { values : [ value ] , options } ) {
1399
1525
return new MongoCredentials ( { ...options . credentials , username : String ( value ) } ) ;
1400
1526
}
1401
1527
} as OptionDescriptor ,
1528
+ username : {
1529
+ rename : 'credentials' ,
1530
+ transform ( { values : [ value ] , options } ) {
1531
+ return new MongoCredentials ( { ...options . credentials , username : String ( value ) } ) ;
1532
+ }
1533
+ } ,
1402
1534
validateOptions : {
1403
1535
type : 'boolean'
1404
1536
} ,
0 commit comments