forked from mongodb/mongo-java-driver
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathConnectionString.java
1817 lines (1681 loc) · 77 KB
/
ConnectionString.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mongodb;
import com.mongodb.annotations.Alpha;
import com.mongodb.annotations.Reason;
import com.mongodb.connection.ClusterSettings;
import com.mongodb.connection.ConnectionPoolSettings;
import com.mongodb.connection.ServerMonitoringMode;
import com.mongodb.connection.ServerSettings;
import com.mongodb.connection.SocketSettings;
import com.mongodb.event.ConnectionCheckOutStartedEvent;
import com.mongodb.event.ConnectionCheckedInEvent;
import com.mongodb.event.ConnectionCheckedOutEvent;
import com.mongodb.event.ConnectionCreatedEvent;
import com.mongodb.event.ConnectionReadyEvent;
import com.mongodb.internal.connection.ServerMonitoringModeUtil;
import com.mongodb.internal.diagnostics.logging.Logger;
import com.mongodb.internal.diagnostics.logging.Loggers;
import com.mongodb.internal.dns.DefaultDnsResolver;
import com.mongodb.lang.Nullable;
import com.mongodb.spi.dns.DnsClient;
import org.bson.UuidRepresentation;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.mongodb.MongoCredential.ALLOWED_HOSTS_KEY;
import static com.mongodb.internal.connection.OidcAuthenticator.OidcValidator.validateCreateOidcCredential;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
/**
* <p>Represents a <a href="https://www.mongodb.com/docs/manual/reference/connection-string/">Connection String</a>.
* The Connection String describes the hosts to be used and options.</p>
*
* <p>The format of the Connection String is:</p>
* <pre>
* mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database.collection][?options]]
* </pre>
* <ul>
* <li>{@code mongodb://} is a required prefix to identify that this is a string in the standard connection format.</li>
* <li>{@code username:password@} are optional. If given, the driver will attempt to login to a database after
* connecting to a database server. For some authentication mechanisms, only the username is specified and the password is not,
* in which case the ":" after the username is left off as well</li>
* <li>{@code host1} is the only required part of the connection string. It identifies a server address to connect to.
* Support for Unix domain sockets was added in 3.7. Note: The path must be urlencoded eg: {@code mongodb://%2Ftmp%2Fmongodb-27017.sock}
* and the {@code jnr.unixsocket} library installed.
* </li>
* <li>{@code :portX} is optional and defaults to :27017 if not provided.</li>
* <li>{@code /database} is the name of the database to login to and thus is only relevant if the
* {@code username:password@} syntax is used. If not specified the "admin" database will be used by default.</li>
* <li>{@code ?options} are connection options. Options are name=value pairs and the pairs
* are separated by "&". For backwards compatibility, ";" is accepted as a separator in addition to "&",
* but should be considered as deprecated.</li>
* </ul>
* <p>An alternative format, using the mongodb+srv protocol, is:
* <pre>
* mongodb+srv://[username:password@]host[/[database][?options]]
* </pre>
* <ul>
* <li>{@code mongodb+srv://} is a required prefix for this format.</li>
* <li>{@code username:password@} are optional. If given, the driver will attempt to login to a database after
* connecting to a database server. For some authentication mechanisms, only the username is specified and the password is not,
* in which case the ":" after the username is left off as well</li>
* <li>{@code host} is the only required part of the URI. It identifies a single host name for which SRV records are looked up
* from a Domain Name Server after prefixing the host name with, by default, {@code "_mongodb._tcp"} ({@code "mongodb"} is the default SRV
* service name, but can be replaced via the {@code srvServiceName} query parameter), The host/port for each SRV record becomes the
* seed list used to connect, as if each one were provided as host/port pair in a URI using the normal mongodb protocol.</li>
* <li>{@code /database} is the name of the database to login to and thus is only relevant if the
* {@code username:password@} syntax is used. If not specified the "admin" database will be used by default.</li>
* <li>{@code ?options} are connection options. Options are name=value pairs and the pairs
* are separated by "&". For backwards compatibility, ";" is accepted as a separator in addition to "&",
* but should be considered as deprecated. Additionally with the mongodb+srv protocol, TXT records are looked up from a Domain Name
* Server for the given host, and the text value of each one is prepended to any options on the URI itself. Because the last specified
* value for any option wins, that means that options provided on the URI will override any that are provided via TXT records.</li>
* </ul>
* <p>The following options are supported (case insensitive):</p>
*
* <p>Server Selection Configuration:</p>
* <ul>
* <li>{@code serverSelectionTimeoutMS=ms}: How long the driver will wait for server selection to succeed before throwing an exception.</li>
* <li>{@code localThresholdMS=ms}: When choosing among multiple MongoDB servers to send a request, the driver will only
* send that request to a server whose ping time is less than or equal to the server with the fastest ping time plus the local
* threshold.</li>
* </ul>
* <p>Server Monitoring Configuration:</p>
* <ul>
* <li>{@code heartbeatFrequencyMS=ms}: The frequency that the driver will attempt to determine the current state of each server in the
* cluster.</li>
* <li>{@code serverMonitoringMode=enum}: The server monitoring mode, which defines the monitoring protocol to use. Enumerated values:
* <ul>
* <li>{@code stream};</li>
* <li>{@code poll};</li>
* <li>{@code auto} - the default.</li>
* </ul>
* </li>
* </ul>
* <p>Replica set configuration:</p>
* <ul>
* <li>{@code replicaSet=name}: Implies that the hosts given are a seed list, and the driver will attempt to find
* all members of the set.</li>
* </ul>
* <p>Connection Configuration:</p>
* <ul>
* <li>{@code ssl=true|false}: Whether to connect using TLS.</li>
* <li>{@code tls=true|false}: Whether to connect using TLS. Supersedes the ssl option</li>
* <li>{@code tlsInsecure=true|false}: If connecting with TLS, this option enables insecure TLS connections. Currently this has the
* same effect of setting tlsAllowInvalidHostnames to true. Other mechanism for relaxing TLS security constraints must be handled in
* the application by customizing the {@link javax.net.ssl.SSLContext}</li>
* <li>{@code sslInvalidHostNameAllowed=true|false}: Whether to allow invalid host names for TLS connections.</li>
* <li>{@code tlsAllowInvalidHostnames=true|false}: Whether to allow invalid host names for TLS connections. Supersedes the
* sslInvalidHostNameAllowed option</li>
* <li>{@code timeoutMS=ms}: Time limit for the full execution of an operation. Note: This parameter is part of an {@linkplain Alpha Alpha API} and may be
* subject to changes or even removal in future releases.</li>
* <li>{@code connectTimeoutMS=ms}: How long a connection can take to be opened before timing out.</li>
* <li>{@code socketTimeoutMS=ms}: How long a receive on a socket can take before timing out.
* This option is the same as {@link SocketSettings#getReadTimeout(TimeUnit)}.
* Deprecated, use {@code timeoutMS} instead.</li>
* <li>{@code maxIdleTimeMS=ms}: Maximum idle time of a pooled connection. A connection that exceeds this limit will be closed</li>
* <li>{@code maxLifeTimeMS=ms}: Maximum life time of a pooled connection. A connection that exceeds this limit will be closed</li>
* </ul>
* <p>Proxy Configuration:</p>
* <ul>
* <li>{@code proxyHost=string}: The SOCKS5 proxy host to establish a connection through.
* It can be provided as a valid IPv4 address, IPv6 address, or a domain name. Required if either proxyPassword, proxyUsername or
* proxyPort are specified</li>
* <li>{@code proxyPort=n}: The port number for the SOCKS5 proxy server. Must be a non-negative integer.</li>
* <li>{@code proxyUsername=string}: Username for authenticating with the proxy server. Required if proxyPassword is specified.</li>
* <li>{@code proxyPassword=string}: Password for authenticating with the proxy server. Required if proxyUsername is specified.</li>
* </ul>
* <p>Connection pool configuration:</p>
* <ul>
* <li>{@code maxPoolSize=n}: The maximum number of connections in the connection pool.</li>
* <li>{@code minPoolSize=n}: The minimum number of connections in the connection pool.</li>
* <li>{@code waitQueueTimeoutMS=ms}: The maximum duration to wait until either:
* an {@linkplain ConnectionCheckedOutEvent in-use connection} becomes {@linkplain ConnectionCheckedInEvent available},
* or a {@linkplain ConnectionCreatedEvent connection is created} and begins to be {@linkplain ConnectionReadyEvent established}.
* See {@link #getMaxWaitTime()} for more details. . Deprecated, use {@code timeoutMS} instead.</li>
* <li>{@code maxConnecting=n}: The maximum number of connections a pool may be establishing concurrently.</li>
* </ul>
* <p>Write concern configuration:</p>
* <ul>
* <li>{@code safe=true|false}
* <ul>
* <li>{@code true}: the driver ensures that all writes are acknowledged by the MongoDB server, or else throws an exception.
* (see also {@code w} and {@code wtimeoutMS}).</li>
* <li>{@code false}: the driver does not ensure that all writes are acknowledged by the MongoDB server.</li>
* </ul>
* </li>
* <li>{@code journal=true|false}
* <ul>
* <li>{@code true}: the driver waits for the server to group commit to the journal file on disk.</li>
* <li>{@code false}: the driver does not wait for the server to group commit to the journal file on disk.</li>
* </ul>
* </li>
* <li>{@code w=wValue}
* <ul>
* <li>The driver adds { w : wValue } to all write commands. Implies {@code safe=true}.</li>
* <li>wValue is typically a number, but can be any string in order to allow for specifications like
* {@code "majority"}</li>
* </ul>
* </li>
* <li>{@code wtimeoutMS=ms}
* <ul>
* <li>The driver adds { wtimeout : ms } to all write commands. Implies {@code safe=true}.</li>
* <li>Used in combination with {@code w}. Deprecated, use {@code timeoutMS} instead</li>
* </ul>
* </li>
* </ul>
* <p>Read preference configuration:</p>
* <ul>
* <li>{@code readPreference=enum}: The read preference for this connection.
* <ul>
* <li>Enumerated values:
* <ul>
* <li>{@code primary}</li>
* <li>{@code primaryPreferred}</li>
* <li>{@code secondary}</li>
* <li>{@code secondaryPreferred}</li>
* <li>{@code nearest}</li>
* </ul>
* </li>
* </ul>
* </li>
* <li>{@code readPreferenceTags=string}. A representation of a tag set as a comma-separated list of colon-separated
* key-value pairs, e.g. {@code "dc:ny,rack:1}". Spaces are stripped from beginning and end of all keys and values.
* To specify a list of tag sets, using multiple readPreferenceTags,
* e.g. {@code readPreferenceTags=dc:ny,rack:1;readPreferenceTags=dc:ny;readPreferenceTags=}
* <ul>
* <li>Note the empty value for the last one, which means match any secondary as a last resort.</li>
* <li>Order matters when using multiple readPreferenceTags.</li>
* </ul>
* </li>
* <li>{@code maxStalenessSeconds=seconds}. The maximum staleness in seconds. For use with any non-primary read preference, the driver
* estimates the staleness of each secondary, based on lastWriteDate values provided in server hello responses, and selects only those
* secondaries whose staleness is less than or equal to maxStalenessSeconds. Not providing the parameter or explicitly setting it to -1
* indicates that there should be no max staleness check. The maximum staleness feature is designed to prevent badly-lagging servers from
* being selected. The staleness estimate is imprecise and shouldn't be used to try to select "up-to-date" secondaries. The minimum value
* is either 90 seconds, or the heartbeat frequency plus 10 seconds, whichever is greatest.
* </li>
* </ul>
* <p>Authentication configuration:</p>
* <ul>
* <li>{@code authMechanism=MONGO-CR|GSSAPI|PLAIN|MONGODB-X509|MONGODB-OIDC}: The authentication mechanism to use if a credential was supplied.
* The default is unspecified, in which case the client will pick the most secure mechanism available based on the sever version. For the
* GSSAPI, MONGODB-X509, and MONGODB-OIDC mechanisms, no password is accepted, only the username.
* </li>
* <li>{@code authSource=string}: The source of the authentication credentials. This is typically the database that
* the credentials have been created. The value defaults to the database specified in the path portion of the connection string.
* If the database is specified in neither place, the default value is "admin". This option is only respected when using the MONGO-CR
* mechanism (the default).
* </li>
* <li>{@code authMechanismProperties=PROPERTY_NAME:PROPERTY_VALUE,PROPERTY_NAME2:PROPERTY_VALUE2}: This option allows authentication
* mechanism properties to be set on the connection string.
* </li>
* <li>{@code gssapiServiceName=string}: This option only applies to the GSSAPI mechanism and is used to alter the service name.
* Deprecated, please use {@code authMechanismProperties=SERVICE_NAME:string} instead.
* </li>
* </ul>
* <p>Server Handshake configuration:</p>
* <ul>
* <li>{@code appName=string}: Sets the logical name of the application. The application name may be used by the client to identify
* the application to the server, for use in server logs, slow query logs, and profile collection.</li>
* </ul>
* <p>Compressor configuration:</p>
* <ul>
* <li>{@code compressors=string}: A comma-separated list of compressors to request from the server. The supported compressors
* currently are 'zlib', 'snappy' and 'zstd'.</li>
* <li>{@code zlibCompressionLevel=integer}: Integer value from -1 to 9 representing the zlib compression level. Lower values will make
* compression faster, while higher values will make compression better.</li>
* </ul>
* <p>SRV configuration:</p>
* <ul>
* <li>{@code srvServiceName=string}: The SRV service name. See {@link ClusterSettings#getSrvServiceName()} for details.</li>
* <li>{@code srvMaxHosts=number}: The maximum number of hosts from the SRV record to connect to.</li>
* </ul>
* <p>General configuration:</p>
* <ul>
* <li>{@code retryWrites=true|false}. If true the driver will retry supported write operations if they fail due to a network error.
* Defaults to true.</li>
* <li>{@code retryReads=true|false}. If true the driver will retry supported read operations if they fail due to a network error.
* Defaults to true.</li>
* <li>{@code uuidRepresentation=unspecified|standard|javaLegacy|csharpLegacy|pythonLegacy}. See
* {@link MongoClientSettings#getUuidRepresentation()} for documentation of semantics of this parameter. Defaults to "javaLegacy", but
* will change to "unspecified" in the next major release.</li>
* <li>{@code directConnection=true|false}. If true the driver will set the connection to be a direct connection to the host.</li>
* <li>{@code loadBalanced=true|false}. If true the driver will assume that it's connecting to MongoDB through a load balancer.</li>
* </ul>
*
* @mongodb.driver.manual reference/connection-string Connection String Format
* @since 3.0.0
*/
public class ConnectionString {
private static final String MONGODB_PREFIX = "mongodb://";
private static final String MONGODB_SRV_PREFIX = "mongodb+srv://";
private static final Set<String> ALLOWED_OPTIONS_IN_TXT_RECORD =
new HashSet<>(asList("authsource", "replicaset", "loadbalanced"));
private static final Logger LOGGER = Loggers.getLogger("uri");
private static final List<String> MECHANISM_KEYS_DISALLOWED_IN_CONNECTION_STRING = Stream.of(ALLOWED_HOSTS_KEY)
.map(k -> k.toLowerCase())
.collect(Collectors.toList());
private final MongoCredential credential;
private final boolean isSrvProtocol;
private final List<String> hosts;
private final String database;
private final String collection;
private final String connectionString;
private Integer srvMaxHosts;
private String srvServiceName;
private Boolean directConnection;
private Boolean loadBalanced;
private ReadPreference readPreference;
private WriteConcern writeConcern;
private Boolean retryWrites;
private Boolean retryReads;
private ReadConcern readConcern;
private Integer minConnectionPoolSize;
private Integer maxConnectionPoolSize;
private Integer maxWaitTime;
private Integer maxConnectionIdleTime;
private Integer maxConnectionLifeTime;
private Integer maxConnecting;
private Integer connectTimeout;
private Long timeout;
private Integer socketTimeout;
private Boolean sslEnabled;
private Boolean sslInvalidHostnameAllowed;
private String proxyHost;
private Integer proxyPort;
private String proxyUsername;
private String proxyPassword;
private String requiredReplicaSetName;
private Integer serverSelectionTimeout;
private Integer localThreshold;
private Integer heartbeatFrequency;
private ServerMonitoringMode serverMonitoringMode;
private String applicationName;
private List<MongoCompressor> compressorList;
private UuidRepresentation uuidRepresentation;
/**
* Creates a ConnectionString from the given string.
*
* @param connectionString the connection string
* @since 3.0
*/
public ConnectionString(final String connectionString) {
this(connectionString, null);
}
/**
* Creates a ConnectionString from the given string with the given {@link DnsClient}.
*
* <p>If setting {@link MongoClientSettings#getDnsClient()} explicitly, care should be taken to call this constructor with the same
* {@link DnsClient}.
*
* @param connectionString the connection string
* @param dnsClient the DNS client with which to resolve TXT record for the mongodb+srv protocol
* @since 4.10
* @see MongoClientSettings#getDnsClient()
*/
public ConnectionString(final String connectionString, @Nullable final DnsClient dnsClient) {
this.connectionString = connectionString;
boolean isMongoDBProtocol = connectionString.startsWith(MONGODB_PREFIX);
isSrvProtocol = connectionString.startsWith(MONGODB_SRV_PREFIX);
if (!isMongoDBProtocol && !isSrvProtocol) {
throw new IllegalArgumentException(format("The connection string is invalid. "
+ "Connection strings must start with either '%s' or '%s", MONGODB_PREFIX, MONGODB_SRV_PREFIX));
}
String unprocessedConnectionString;
if (isMongoDBProtocol) {
unprocessedConnectionString = connectionString.substring(MONGODB_PREFIX.length());
} else {
unprocessedConnectionString = connectionString.substring(MONGODB_SRV_PREFIX.length());
}
// Split out the user and host information
String userAndHostInformation;
int firstForwardSlashIdx = unprocessedConnectionString.indexOf("/");
int firstQuestionMarkIdx = unprocessedConnectionString.indexOf("?");
if (firstQuestionMarkIdx == -1 && firstForwardSlashIdx == -1) {
userAndHostInformation = unprocessedConnectionString;
unprocessedConnectionString = "";
} else if (firstQuestionMarkIdx != -1 && (firstForwardSlashIdx == -1 || firstQuestionMarkIdx < firstForwardSlashIdx)) {
// there is a question mark, and there is no slash or the question mark comes before any slash
userAndHostInformation = unprocessedConnectionString.substring(0, firstQuestionMarkIdx);
unprocessedConnectionString = unprocessedConnectionString.substring(firstQuestionMarkIdx);
} else {
userAndHostInformation = unprocessedConnectionString.substring(0, firstForwardSlashIdx);
unprocessedConnectionString = unprocessedConnectionString.substring(firstForwardSlashIdx + 1);
}
// Split the user and host information
String userInfo;
String hostIdentifier;
String userName = null;
char[] password = null;
int idx = userAndHostInformation.lastIndexOf("@");
if (idx > 0) {
userInfo = userAndHostInformation.substring(0, idx).replace("+", "%2B");
hostIdentifier = userAndHostInformation.substring(idx + 1);
int colonCount = countOccurrences(userInfo, ":");
if (userInfo.contains("@") || colonCount > 1) {
throw new IllegalArgumentException("The connection string contains invalid user information. "
+ "If the username or password contains a colon (:) or an at-sign (@) then it must be urlencoded");
}
if (colonCount == 0) {
userName = urldecode(userInfo);
} else {
idx = userInfo.indexOf(":");
if (idx == 0) {
throw new IllegalArgumentException("No username is provided in the connection string");
}
userName = urldecode(userInfo.substring(0, idx));
password = urldecode(userInfo.substring(idx + 1), true).toCharArray();
}
} else if (idx == 0) {
throw new IllegalArgumentException("The connection string contains an at-sign (@) without a user name");
} else {
hostIdentifier = userAndHostInformation;
}
// Validate the hosts
List<String> unresolvedHosts = unmodifiableList(parseHosts(asList(hostIdentifier.split(","))));
if (isSrvProtocol) {
if (unresolvedHosts.size() > 1) {
throw new IllegalArgumentException("Only one host allowed when using mongodb+srv protocol");
}
if (unresolvedHosts.get(0).contains(":")) {
throw new IllegalArgumentException("Host for when using mongodb+srv protocol can not contain a port");
}
}
this.hosts = unresolvedHosts;
// Process the authDB section
String nsPart;
idx = unprocessedConnectionString.indexOf("?");
if (idx == -1) {
nsPart = unprocessedConnectionString;
unprocessedConnectionString = "";
} else {
nsPart = unprocessedConnectionString.substring(0, idx);
unprocessedConnectionString = unprocessedConnectionString.substring(idx + 1);
}
if (nsPart.length() > 0) {
nsPart = urldecode(nsPart);
idx = nsPart.indexOf(".");
if (idx < 0) {
database = nsPart;
collection = null;
} else {
database = nsPart.substring(0, idx);
collection = nsPart.substring(idx + 1);
}
MongoNamespace.checkDatabaseNameValidity(database);
} else {
database = null;
collection = null;
}
String txtRecordsQueryParameters = isSrvProtocol
? new DefaultDnsResolver(dnsClient).resolveAdditionalQueryParametersFromTxtRecords(unresolvedHosts.get(0)) : "";
String connectionStringQueryParameters = unprocessedConnectionString;
Map<String, List<String>> connectionStringOptionsMap = parseOptions(connectionStringQueryParameters);
Map<String, List<String>> txtRecordsOptionsMap = parseOptions(txtRecordsQueryParameters);
if (!ALLOWED_OPTIONS_IN_TXT_RECORD.containsAll(txtRecordsOptionsMap.keySet())) {
throw new MongoConfigurationException(format("A TXT record is only permitted to contain the keys %s, but the TXT record for "
+ "'%s' contains the keys %s", ALLOWED_OPTIONS_IN_TXT_RECORD, unresolvedHosts.get(0), txtRecordsOptionsMap.keySet()));
}
Map<String, List<String>> combinedOptionsMaps = combineOptionsMaps(txtRecordsOptionsMap, connectionStringOptionsMap);
if (isSrvProtocol && !(combinedOptionsMaps.containsKey("tls") || combinedOptionsMaps.containsKey("ssl"))) {
combinedOptionsMaps.put("tls", singletonList("true"));
}
translateOptions(combinedOptionsMaps);
if (!isSrvProtocol && srvMaxHosts != null) {
throw new IllegalArgumentException("srvMaxHosts can only be specified with mongodb+srv protocol");
}
if (!isSrvProtocol && srvServiceName != null) {
throw new IllegalArgumentException("srvServiceName can only be specified with mongodb+srv protocol");
}
if (directConnection != null && directConnection) {
if (isSrvProtocol) {
throw new IllegalArgumentException("Direct connections are not supported when using mongodb+srv protocol");
} else if (hosts.size() > 1) {
throw new IllegalArgumentException("Direct connections are not supported when using multiple hosts");
}
}
if (loadBalanced != null && loadBalanced) {
if (directConnection != null && directConnection) {
throw new IllegalArgumentException("directConnection=true can not be specified with loadBalanced=true");
}
if (requiredReplicaSetName != null) {
throw new IllegalArgumentException("replicaSet can not be specified with loadBalanced=true");
}
if (hosts.size() > 1) {
throw new IllegalArgumentException("Only one host can be specified with loadBalanced=true");
}
if (srvMaxHosts != null && srvMaxHosts > 0) {
throw new IllegalArgumentException("srvMaxHosts can not be specified with loadBalanced=true");
}
}
if (requiredReplicaSetName != null && srvMaxHosts != null && srvMaxHosts > 0) {
throw new IllegalArgumentException("srvMaxHosts can not be specified with replica set name");
}
validateProxyParameters();
credential = createCredentials(combinedOptionsMaps, userName, password);
warnOnUnsupportedOptions(combinedOptionsMaps);
}
private static final Set<String> GENERAL_OPTIONS_KEYS = new LinkedHashSet<>();
private static final Set<String> AUTH_KEYS = new HashSet<>();
private static final Set<String> READ_PREFERENCE_KEYS = new HashSet<>();
private static final Set<String> WRITE_CONCERN_KEYS = new HashSet<>();
private static final Set<String> COMPRESSOR_KEYS = new HashSet<>();
private static final Set<String> ALL_KEYS = new HashSet<>();
static {
GENERAL_OPTIONS_KEYS.add("minpoolsize");
GENERAL_OPTIONS_KEYS.add("maxpoolsize");
GENERAL_OPTIONS_KEYS.add("timeoutms");
GENERAL_OPTIONS_KEYS.add("sockettimeoutms");
GENERAL_OPTIONS_KEYS.add("waitqueuetimeoutms");
GENERAL_OPTIONS_KEYS.add("connecttimeoutms");
GENERAL_OPTIONS_KEYS.add("maxidletimems");
GENERAL_OPTIONS_KEYS.add("maxlifetimems");
GENERAL_OPTIONS_KEYS.add("maxconnecting");
// Order matters here: Having tls after ssl means than the tls option will supersede the ssl option when both are set
GENERAL_OPTIONS_KEYS.add("ssl");
GENERAL_OPTIONS_KEYS.add("tls");
// Order matters here: Having tlsinsecure before sslinvalidhostnameallowed and tlsallowinvalidhostnames means that those options
// will supersede this one when both are set.
GENERAL_OPTIONS_KEYS.add("tlsinsecure");
// Order matters here: Having tlsallowinvalidhostnames after sslinvalidhostnameallowed means than the tlsallowinvalidhostnames
// option will supersede the sslinvalidhostnameallowed option when both are set
GENERAL_OPTIONS_KEYS.add("sslinvalidhostnameallowed");
GENERAL_OPTIONS_KEYS.add("tlsallowinvalidhostnames");
//Socks5 proxy settings
GENERAL_OPTIONS_KEYS.add("proxyhost");
GENERAL_OPTIONS_KEYS.add("proxyport");
GENERAL_OPTIONS_KEYS.add("proxyusername");
GENERAL_OPTIONS_KEYS.add("proxypassword");
GENERAL_OPTIONS_KEYS.add("replicaset");
GENERAL_OPTIONS_KEYS.add("readconcernlevel");
GENERAL_OPTIONS_KEYS.add("serverselectiontimeoutms");
GENERAL_OPTIONS_KEYS.add("localthresholdms");
GENERAL_OPTIONS_KEYS.add("heartbeatfrequencyms");
GENERAL_OPTIONS_KEYS.add("servermonitoringmode");
GENERAL_OPTIONS_KEYS.add("retrywrites");
GENERAL_OPTIONS_KEYS.add("retryreads");
GENERAL_OPTIONS_KEYS.add("appname");
GENERAL_OPTIONS_KEYS.add("uuidrepresentation");
GENERAL_OPTIONS_KEYS.add("directconnection");
GENERAL_OPTIONS_KEYS.add("loadbalanced");
GENERAL_OPTIONS_KEYS.add("srvmaxhosts");
GENERAL_OPTIONS_KEYS.add("srvservicename");
COMPRESSOR_KEYS.add("compressors");
COMPRESSOR_KEYS.add("zlibcompressionlevel");
READ_PREFERENCE_KEYS.add("readpreference");
READ_PREFERENCE_KEYS.add("readpreferencetags");
READ_PREFERENCE_KEYS.add("maxstalenessseconds");
WRITE_CONCERN_KEYS.add("safe");
WRITE_CONCERN_KEYS.add("w");
WRITE_CONCERN_KEYS.add("wtimeoutms");
WRITE_CONCERN_KEYS.add("journal");
AUTH_KEYS.add("authmechanism");
AUTH_KEYS.add("authsource");
AUTH_KEYS.add("gssapiservicename");
AUTH_KEYS.add("authmechanismproperties");
ALL_KEYS.addAll(GENERAL_OPTIONS_KEYS);
ALL_KEYS.addAll(AUTH_KEYS);
ALL_KEYS.addAll(READ_PREFERENCE_KEYS);
ALL_KEYS.addAll(WRITE_CONCERN_KEYS);
ALL_KEYS.addAll(COMPRESSOR_KEYS);
}
// Any options contained in the connection string completely replace the corresponding options specified in TXT records,
// even for options which multiple values, e.g. readPreferenceTags
private Map<String, List<String>> combineOptionsMaps(final Map<String, List<String>> txtRecordsOptionsMap,
final Map<String, List<String>> connectionStringOptionsMap) {
Map<String, List<String>> combinedOptionsMaps = new HashMap<>(txtRecordsOptionsMap);
combinedOptionsMaps.putAll(connectionStringOptionsMap);
return combinedOptionsMaps;
}
private void warnOnUnsupportedOptions(final Map<String, List<String>> optionsMap) {
if (LOGGER.isWarnEnabled()) {
optionsMap.keySet()
.stream()
.filter(k -> !ALL_KEYS.contains(k))
.forEach(k -> LOGGER.warn(format("Connection string contains unsupported option '%s'.", k)));
}
}
private void translateOptions(final Map<String, List<String>> optionsMap) {
boolean tlsInsecureSet = false;
boolean tlsAllowInvalidHostnamesSet = false;
for (final String key : GENERAL_OPTIONS_KEYS) {
String value = getLastValue(optionsMap, key);
if (value == null) {
continue;
}
switch (key) {
case "maxpoolsize":
maxConnectionPoolSize = parseInteger(value, "maxpoolsize");
break;
case "minpoolsize":
minConnectionPoolSize = parseInteger(value, "minpoolsize");
break;
case "maxidletimems":
maxConnectionIdleTime = parseInteger(value, "maxidletimems");
break;
case "maxlifetimems":
maxConnectionLifeTime = parseInteger(value, "maxlifetimems");
break;
case "maxconnecting":
maxConnecting = parseInteger(value, "maxConnecting");
break;
case "waitqueuetimeoutms":
maxWaitTime = parseInteger(value, "waitqueuetimeoutms");
break;
case "connecttimeoutms":
connectTimeout = parseInteger(value, "connecttimeoutms");
break;
case "sockettimeoutms":
socketTimeout = parseInteger(value, "sockettimeoutms");
break;
case "timeoutms":
timeout = parseLong(value, "timeoutms");
break;
case "proxyhost":
proxyHost = value;
break;
case "proxyport":
proxyPort = parseInteger(value, "proxyPort");
break;
case "proxyusername":
proxyUsername = value;
break;
case "proxypassword":
proxyPassword = value;
break;
case "tlsallowinvalidhostnames":
sslInvalidHostnameAllowed = parseBoolean(value, "tlsAllowInvalidHostnames");
tlsAllowInvalidHostnamesSet = true;
break;
case "sslinvalidhostnameallowed":
sslInvalidHostnameAllowed = parseBoolean(value, "sslinvalidhostnameallowed");
tlsAllowInvalidHostnamesSet = true;
break;
case "tlsinsecure":
sslInvalidHostnameAllowed = parseBoolean(value, "tlsinsecure");
tlsInsecureSet = true;
break;
case "ssl":
initializeSslEnabled("ssl", value);
break;
case "tls":
initializeSslEnabled("tls", value);
break;
case "replicaset":
requiredReplicaSetName = value;
break;
case "readconcernlevel":
readConcern = new ReadConcern(ReadConcernLevel.fromString(value));
break;
case "serverselectiontimeoutms":
serverSelectionTimeout = parseInteger(value, "serverselectiontimeoutms");
break;
case "localthresholdms":
localThreshold = parseInteger(value, "localthresholdms");
break;
case "heartbeatfrequencyms":
heartbeatFrequency = parseInteger(value, "heartbeatfrequencyms");
break;
case "servermonitoringmode":
serverMonitoringMode = ServerMonitoringModeUtil.fromString(value);
break;
case "appname":
applicationName = value;
break;
case "retrywrites":
retryWrites = parseBoolean(value, "retrywrites");
break;
case "retryreads":
retryReads = parseBoolean(value, "retryreads");
break;
case "uuidrepresentation":
uuidRepresentation = createUuidRepresentation(value);
break;
case "directconnection":
directConnection = parseBoolean(value, "directconnection");
break;
case "loadbalanced":
loadBalanced = parseBoolean(value, "loadbalanced");
break;
case "srvmaxhosts":
srvMaxHosts = parseInteger(value, "srvmaxhosts");
if (srvMaxHosts < 0) {
throw new IllegalArgumentException("srvMaxHosts must be >= 0");
}
break;
case "srvservicename":
srvServiceName = value;
break;
default:
break;
}
}
if (tlsInsecureSet && tlsAllowInvalidHostnamesSet) {
throw new IllegalArgumentException("tlsAllowInvalidHostnames or sslInvalidHostnameAllowed set along with tlsInsecure "
+ "is not allowed");
}
writeConcern = createWriteConcern(optionsMap);
readPreference = createReadPreference(optionsMap);
compressorList = createCompressors(optionsMap);
}
private void initializeSslEnabled(final String key, final String value) {
Boolean booleanValue = parseBoolean(value, key);
if (sslEnabled != null && !sslEnabled.equals(booleanValue)) {
throw new IllegalArgumentException("Conflicting tls and ssl parameter values are not allowed");
}
sslEnabled = booleanValue;
}
private List<MongoCompressor> createCompressors(final Map<String, List<String>> optionsMap) {
String compressors = "";
Integer zlibCompressionLevel = null;
for (final String key : COMPRESSOR_KEYS) {
String value = getLastValue(optionsMap, key);
if (value == null) {
continue;
}
if (key.equals("compressors")) {
compressors = value;
} else if (key.equals("zlibcompressionlevel")) {
zlibCompressionLevel = Integer.parseInt(value);
}
}
return buildCompressors(compressors, zlibCompressionLevel);
}
private List<MongoCompressor> buildCompressors(final String compressors, @Nullable final Integer zlibCompressionLevel) {
List<MongoCompressor> compressorsList = new ArrayList<>();
for (String cur : compressors.split(",")) {
if (cur.equals("zlib")) {
MongoCompressor zlibCompressor = MongoCompressor.createZlibCompressor();
if (zlibCompressionLevel != null) {
zlibCompressor = zlibCompressor.withProperty(MongoCompressor.LEVEL, zlibCompressionLevel);
}
compressorsList.add(zlibCompressor);
} else if (cur.equals("snappy")) {
compressorsList.add(MongoCompressor.createSnappyCompressor());
} else if (cur.equals("zstd")) {
compressorsList.add(MongoCompressor.createZstdCompressor());
} else if (!cur.isEmpty()) {
throw new IllegalArgumentException("Unsupported compressor '" + cur + "'");
}
}
return unmodifiableList(compressorsList);
}
@Nullable
private WriteConcern createWriteConcern(final Map<String, List<String>> optionsMap) {
String w = null;
Integer wTimeout = null;
Boolean safe = null;
Boolean journal = null;
for (final String key : WRITE_CONCERN_KEYS) {
String value = getLastValue(optionsMap, key);
if (value == null) {
continue;
}
switch (key) {
case "safe":
safe = parseBoolean(value, "safe");
break;
case "w":
w = value;
break;
case "wtimeoutms":
wTimeout = Integer.parseInt(value);
break;
case "journal":
journal = parseBoolean(value, "journal");
break;
default:
break;
}
}
return buildWriteConcern(safe, w, wTimeout, journal);
}
@Nullable
private ReadPreference createReadPreference(final Map<String, List<String>> optionsMap) {
String readPreferenceType = null;
List<TagSet> tagSetList = new ArrayList<>();
long maxStalenessSeconds = -1;
for (final String key : READ_PREFERENCE_KEYS) {
String value = getLastValue(optionsMap, key);
if (value == null) {
continue;
}
switch (key) {
case "readpreference":
readPreferenceType = value;
break;
case "maxstalenessseconds":
maxStalenessSeconds = parseInteger(value, "maxstalenessseconds");
break;
case "readpreferencetags":
for (final String cur : optionsMap.get(key)) {
TagSet tagSet = getTags(cur.trim());
tagSetList.add(tagSet);
}
break;
default:
break;
}
}
return buildReadPreference(readPreferenceType, tagSetList, maxStalenessSeconds);
}
private UuidRepresentation createUuidRepresentation(final String value) {
if (value.equalsIgnoreCase("unspecified")) {
return UuidRepresentation.UNSPECIFIED;
}
if (value.equalsIgnoreCase("javaLegacy")) {
return UuidRepresentation.JAVA_LEGACY;
}
if (value.equalsIgnoreCase("csharpLegacy")) {
return UuidRepresentation.C_SHARP_LEGACY;
}
if (value.equalsIgnoreCase("pythonLegacy")) {
return UuidRepresentation.PYTHON_LEGACY;
}
if (value.equalsIgnoreCase("standard")) {
return UuidRepresentation.STANDARD;
}
throw new IllegalArgumentException("Unknown uuid representation: " + value);
}
@Nullable
private MongoCredential createCredentials(final Map<String, List<String>> optionsMap, @Nullable final String userName,
@Nullable final char[] password) {
AuthenticationMechanism mechanism = null;
String authSource = null;
String gssapiServiceName = null;
String authMechanismProperties = null;
for (final String key : AUTH_KEYS) {
String value = getLastValue(optionsMap, key);
if (value == null) {
continue;
}
switch (key) {
case "authmechanism":
if (value.equals("MONGODB-CR")) {
if (userName == null) {
throw new IllegalArgumentException("username can not be null");
}
LOGGER.warn("Deprecated MONGDOB-CR authentication mechanism used in connection string");
} else {
mechanism = AuthenticationMechanism.fromMechanismName(value);
}
break;
case "authsource":
if (value.equals("")) {
throw new IllegalArgumentException("authSource can not be an empty string");
}
authSource = value;
break;
case "gssapiservicename":
gssapiServiceName = value;
break;
case "authmechanismproperties":
authMechanismProperties = value;
break;
default:
break;
}
}
MongoCredential credential = null;
if (mechanism != null) {
credential = createMongoCredentialWithMechanism(mechanism, userName, password, authSource, gssapiServiceName);
} else if (userName != null) {
credential = MongoCredential.createCredential(userName,
getAuthSourceOrDefault(authSource, database != null ? database : "admin"), password);
}
if (credential != null && authMechanismProperties != null) {
for (String part : authMechanismProperties.split(",")) {
String[] mechanismPropertyKeyValue = part.split(":", 2);
if (mechanismPropertyKeyValue.length != 2) {
throw new IllegalArgumentException(format("The connection string contains invalid authentication properties. "
+ "'%s' is not a key value pair", part));
}
String key = mechanismPropertyKeyValue[0].trim().toLowerCase();
String value = mechanismPropertyKeyValue[1].trim();
if (MECHANISM_KEYS_DISALLOWED_IN_CONNECTION_STRING.contains(key)) {
throw new IllegalArgumentException(format("The connection string contains disallowed mechanism properties. "
+ "'%s' must be set on the credential programmatically.", key));
}
if (key.equals("canonicalize_host_name")) {
credential = credential.withMechanismProperty(key, Boolean.valueOf(value));
} else {
credential = credential.withMechanismProperty(key, value);
}
}
}
return credential;
}
private MongoCredential createMongoCredentialWithMechanism(final AuthenticationMechanism mechanism, final String userName,
@Nullable final char[] password,
@Nullable final String authSource,
@Nullable final String gssapiServiceName) {
MongoCredential credential;
String mechanismAuthSource;
switch (mechanism) {
case PLAIN:
mechanismAuthSource = getAuthSourceOrDefault(authSource, database != null ? database : "$external");
break;
case GSSAPI:
case MONGODB_X509:
mechanismAuthSource = getAuthSourceOrDefault(authSource, "$external");
if (!mechanismAuthSource.equals("$external")) {
throw new IllegalArgumentException(format("Invalid authSource for %s, it must be '$external'", mechanism));
}
break;
default:
mechanismAuthSource = getAuthSourceOrDefault(authSource, database != null ? database : "admin");
}
switch (mechanism) {
case GSSAPI:
credential = MongoCredential.createGSSAPICredential(userName);
if (gssapiServiceName != null) {
credential = credential.withMechanismProperty("SERVICE_NAME", gssapiServiceName);
}
if (password != null && LOGGER.isWarnEnabled()) {
LOGGER.warn("Password in connection string not used with MONGODB_X509 authentication mechanism.");
}
break;
case PLAIN:
credential = MongoCredential.createPlainCredential(userName, mechanismAuthSource, password);
break;
case MONGODB_X509:
if (password != null) {
throw new IllegalArgumentException("Invalid mechanism, MONGODB_x509 does not support passwords");
}
credential = MongoCredential.createMongoX509Credential(userName);
break;
case SCRAM_SHA_1:
credential = MongoCredential.createScramSha1Credential(userName, mechanismAuthSource, password);
break;
case SCRAM_SHA_256:
credential = MongoCredential.createScramSha256Credential(userName, mechanismAuthSource, password);
break;
case MONGODB_AWS:
credential = MongoCredential.createAwsCredential(userName, password);
break;
case MONGODB_OIDC:
validateCreateOidcCredential(password);
credential = MongoCredential.createOidcCredential(userName);