diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES
index acca60ca973..e52933651b4 100644
--- a/THIRD-PARTY-NOTICES
+++ b/THIRD-PARTY-NOTICES
@@ -200,3 +200,4 @@ https://github.com/mongodb/mongo-java-driver.
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.
+
diff --git a/driver-core/src/main/com/mongodb/ConnectionString.java b/driver-core/src/main/com/mongodb/ConnectionString.java
index c7142e19517..1cd77140920 100644
--- a/driver-core/src/main/com/mongodb/ConnectionString.java
+++ b/driver-core/src/main/com/mongodb/ConnectionString.java
@@ -54,6 +54,7 @@
import java.util.stream.Stream;
import static com.mongodb.MongoCredential.ALLOWED_HOSTS_KEY;
+import static com.mongodb.internal.connection.DomainNameUtils.normalizeSrvAllowedHostsSuffix;
import static com.mongodb.internal.connection.OidcAuthenticator.OidcValidator.validateCreateOidcCredential;
import static java.lang.String.format;
import static java.util.Arrays.asList;
@@ -61,7 +62,6 @@
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
-
/**
*
Represents a Connection String.
* The Connection String describes the hosts to be used and options.
@@ -266,6 +266,13 @@
*
* - {@code srvServiceName=string}: The SRV service name. See {@link ClusterSettings#getSrvServiceName()} for details.
* - {@code srvMaxHosts=n}: The maximum number of hosts from the SRV record to connect to.
+ * - {@code srvAllowedHostsSuffix=string}: The hostname suffix used to validate hosts returned via SRV lookup, replacing the domain
+ * inferred from the SRV host name. Only valid with the mongodb+srv protocol. WARNING: Modifying the default SRV domain name
+ * validation can create vulnerabilities. This option relaxes a built-in DNS spoofing safeguard. Use the most specific suffix possible for
+ * your deployment rather than a broad company-wide domain. For example, instead of {@code "mongodb+srv://cluster.test.internal.example
+ * .com/?srvAllowedHostsSuffix=.example.com"} which would accept any host across the entire domain, scope it further like so:
+ * {@code "mongodb+srv://cluster.test.internal.example.com/?srvAllowedHostsSuffix=.internal.example.com"}.
+ *
*
* General configuration:
*
@@ -309,6 +316,7 @@ public class ConnectionString {
private Integer srvMaxHosts;
private String srvServiceName;
+ private String srvAllowedHostsSuffix;
private Boolean directConnection;
private Boolean loadBalanced;
private ReadPreference readPreference;
@@ -488,6 +496,14 @@ public ConnectionString(final String connectionString, @Nullable final DnsClient
throw new IllegalArgumentException("srvServiceName can only be specified with mongodb+srv protocol");
}
+ if (!isSrvProtocol && srvAllowedHostsSuffix != null) {
+ throw new IllegalArgumentException("srvAllowedHostsSuffix can only be specified with mongodb+srv protocol");
+ }
+
+ if (srvAllowedHostsSuffix != null) {
+ srvAllowedHostsSuffix = normalizeSrvAllowedHostsSuffix(srvAllowedHostsSuffix);
+ }
+
if (directConnection != null && directConnection) {
if (isSrvProtocol) {
throw new IllegalArgumentException("Direct connections are not supported when using mongodb+srv protocol");
@@ -579,6 +595,7 @@ public ConnectionString(final String connectionString, @Nullable final DnsClient
GENERAL_OPTIONS_KEYS.add("srvmaxhosts");
GENERAL_OPTIONS_KEYS.add("srvservicename");
+ GENERAL_OPTIONS_KEYS.add("srvallowedhostssuffix");
COMPRESSOR_KEYS.add("compressors");
COMPRESSOR_KEYS.add("zlibcompressionlevel");
@@ -744,6 +761,9 @@ private void translateOptions(final Map> optionsMap) {
case "srvservicename":
srvServiceName = value;
break;
+ case "srvallowedhostssuffix":
+ srvAllowedHostsSuffix = value;
+ break;
default:
break;
}
@@ -1375,6 +1395,22 @@ public String getSrvServiceName() {
return srvServiceName;
}
+ /**
+ * Gets the SRV allowed hosts suffix.
+ *
+ * If present, its value is used as the domain for SRV host name validation, replacing the domain inferred from
+ * the SRV host name. The value is normalized: a leading {@code "."} is prepended if absent, so the returned value
+ * always begins with {@code "."}.
+ *
+ * @return the normalized SRV allowed hosts suffix, always beginning with {@code "."}. Defaults to null.
+ * @since 5.13
+ * @see ClusterSettings#getSrvAllowedHostsSuffix()
+ */
+ @Nullable
+ public String getSrvAllowedHostsSuffix() {
+ return srvAllowedHostsSuffix;
+ }
+
/**
* Gets the list of hosts
*
@@ -1875,7 +1911,8 @@ public boolean equals(final Object o) {
&& Objects.equals(compressorList, that.compressorList)
&& Objects.equals(uuidRepresentation, that.uuidRepresentation)
&& Objects.equals(srvServiceName, that.srvServiceName)
- && Objects.equals(srvMaxHosts, that.srvMaxHosts);
+ && Objects.equals(srvMaxHosts, that.srvMaxHosts)
+ && Objects.equals(srvAllowedHostsSuffix, that.srvAllowedHostsSuffix);
}
@Override
@@ -1884,7 +1921,7 @@ public int hashCode() {
writeConcern, retryWrites, retryReads, maxAdaptiveRetries, enableOverloadRetargeting, readConcern, minConnectionPoolSize, maxConnectionPoolSize, maxWaitTime,
maxConnectionIdleTime, maxConnectionLifeTime, maxConnecting, connectTimeout, timeout, socketTimeout, sslEnabled,
sslInvalidHostnameAllowed, requiredReplicaSetName, serverSelectionTimeout, localThreshold, heartbeatFrequency,
- serverMonitoringMode, applicationName, compressorList, uuidRepresentation, srvServiceName, srvMaxHosts, proxyHost,
- proxyPort, proxyUsername, proxyPassword);
+ serverMonitoringMode, applicationName, compressorList, uuidRepresentation, srvServiceName, srvMaxHosts,
+ srvAllowedHostsSuffix, proxyHost, proxyPort, proxyUsername, proxyPassword);
}
}
diff --git a/driver-core/src/main/com/mongodb/connection/ClusterSettings.java b/driver-core/src/main/com/mongodb/connection/ClusterSettings.java
index 01e5c140441..7a6cc7f9b3a 100644
--- a/driver-core/src/main/com/mongodb/connection/ClusterSettings.java
+++ b/driver-core/src/main/com/mongodb/connection/ClusterSettings.java
@@ -35,6 +35,7 @@
import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;
+import static com.mongodb.internal.connection.DomainNameUtils.normalizeSrvAllowedHostsSuffix;
import static com.mongodb.internal.connection.ServerAddressHelper.createServerAddress;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
@@ -50,6 +51,7 @@ public final class ClusterSettings {
private final String srvHost;
private final Integer srvMaxHosts;
private final String srvServiceName;
+ private final String srvAllowedHostsSuffix;
private final List hosts;
private final ClusterConnectionMode mode;
private final ClusterType requiredClusterType;
@@ -88,6 +90,7 @@ public static final class Builder {
private String srvHost;
private Integer srvMaxHosts;
private String srvServiceName = "mongodb";
+ private String srvAllowedHostsSuffix;
private List hosts = DEFAULT_HOSTS;
private ClusterConnectionMode mode;
private ClusterType requiredClusterType = ClusterType.UNKNOWN;
@@ -114,6 +117,7 @@ public Builder applySettings(final ClusterSettings clusterSettings) {
srvHost = clusterSettings.srvHost;
srvServiceName = clusterSettings.srvServiceName;
srvMaxHosts = clusterSettings.srvMaxHosts;
+ srvAllowedHostsSuffix = clusterSettings.srvAllowedHostsSuffix;
hosts = clusterSettings.hosts;
mode = clusterSettings.mode;
requiredReplicaSetName = clusterSettings.requiredReplicaSetName;
@@ -185,6 +189,26 @@ public Builder srvServiceName(final String srvServiceName) {
return this;
}
+ /**
+ * Sets the SRV allowed hosts suffix used to validate hosts returned via SRV lookup.
+ *
+ * If set, its value is used as the domain for SRV host name validation, replacing the domain inferred from
+ * the SRV host name. The value is normalized: a leading {@code "."} is prepended if absent, so
+ * {@link #getSrvAllowedHostsSuffix()} always returns a value beginning with {@code "."}. This setting is only
+ * used with SRV. Specifying an overly broad suffix (for example a bare TLD) weakens SRV host name validation and
+ * is the responsibility of the caller.
+ *
+ * @param srvAllowedHostsSuffix the SRV allowed hosts suffix; may not be null or empty
+ * @return this
+ * @since 5.13
+ * @see #getSrvAllowedHostsSuffix()
+ */
+ public Builder srvAllowedHostsSuffix(final String srvAllowedHostsSuffix) {
+ notNull("srvAllowedHostsSuffix", srvAllowedHostsSuffix);
+ this.srvAllowedHostsSuffix = normalizeSrvAllowedHostsSuffix(srvAllowedHostsSuffix);
+ return this;
+ }
+
/**
* Sets the hosts for the cluster. Any duplicate server addresses are removed from the list.
*
@@ -328,6 +352,7 @@ public Builder applyConnectionString(final ConnectionString connectionString) {
mode(ClusterConnectionMode.LOAD_BALANCED);
if (connectionString.isSrvProtocol()) {
srvHost(connectionString.getHosts().get(0));
+ applySrvConnectionStringOptions(connectionString);
} else {
hosts(singletonList(createServerAddress(connectionString.getHosts().get(0))));
}
@@ -338,10 +363,7 @@ public Builder applyConnectionString(final ConnectionString connectionString) {
if (srvMaxHosts != null) {
srvMaxHosts(srvMaxHosts);
}
- String srvServiceName = connectionString.getSrvServiceName();
- if (srvServiceName != null) {
- srvServiceName(srvServiceName);
- }
+ applySrvConnectionStringOptions(connectionString);
} else if (directConnection != null) {
mode(directConnection ? ClusterConnectionMode.SINGLE : ClusterConnectionMode.MULTIPLE);
List hosts = directConnection ? singletonList(connectionString.getHosts().get(0)) : connectionString.getHosts();
@@ -367,6 +389,19 @@ public Builder applyConnectionString(final ConnectionString connectionString) {
return this;
}
+ // Applies the SRV options shared by the load-balanced and multi-server SRV paths. srvMaxHosts is intentionally
+ // not applied here, as it is only valid for the multi-server path.
+ private void applySrvConnectionStringOptions(final ConnectionString connectionString) {
+ String srvServiceName = connectionString.getSrvServiceName();
+ if (srvServiceName != null) {
+ srvServiceName(srvServiceName);
+ }
+ String srvAllowedHostsSuffix = connectionString.getSrvAllowedHostsSuffix();
+ if (srvAllowedHostsSuffix != null) {
+ srvAllowedHostsSuffix(srvAllowedHostsSuffix);
+ }
+ }
+
/**
* Build the settings from the builder.
*
@@ -421,6 +456,23 @@ public String getSrvServiceName() {
return srvServiceName;
}
+ /**
+ * Gets the SRV allowed hosts suffix used to validate hosts returned via SRV lookup.
+ *
+ * If present, its value is used as the domain for SRV host name validation, replacing the domain inferred from
+ * the SRV host name. The value is normalized to always begin with {@code "."}.
+ *
+ * WARNING: Modifying the default SRV domain name validation can create vulnerabilities.
+ *
+ * @return the normalized SRV allowed hosts suffix, always beginning with {@code "."}. Defaults to null.
+ * @since 5.13
+ * @see Builder#srvAllowedHostsSuffix(String)
+ */
+ @Nullable
+ public String getSrvAllowedHostsSuffix() {
+ return srvAllowedHostsSuffix;
+ }
+
/**
* Gets the seed list of hosts for the cluster.
*
@@ -554,6 +606,7 @@ public boolean equals(final Object o) {
&& Objects.equals(srvHost, that.srvHost)
&& Objects.equals(srvMaxHosts, that.srvMaxHosts)
&& srvServiceName.equals(that.srvServiceName)
+ && Objects.equals(srvAllowedHostsSuffix, that.srvAllowedHostsSuffix)
&& hosts.equals(that.hosts)
&& mode == that.mode
&& requiredClusterType == that.requiredClusterType
@@ -564,8 +617,8 @@ public boolean equals(final Object o) {
@Override
public int hashCode() {
- return Objects.hash(srvHost, srvMaxHosts, srvServiceName, hosts, mode, requiredClusterType, requiredReplicaSetName, serverSelector,
- localThresholdMS, serverSelectionTimeoutMS, clusterListeners);
+ return Objects.hash(srvHost, srvMaxHosts, srvServiceName, srvAllowedHostsSuffix, hosts, mode, requiredClusterType,
+ requiredReplicaSetName, serverSelector, localThresholdMS, serverSelectionTimeoutMS, clusterListeners);
}
@Override
@@ -575,6 +628,7 @@ public String toString() {
+ (srvHost == null ? "" : ", srvHost=" + srvHost)
+ (srvServiceName == null ? "" : ", srvServiceName=" + srvServiceName)
+ (srvMaxHosts == null ? "" : ", srvMaxHosts=" + srvMaxHosts)
+ + (srvAllowedHostsSuffix == null ? "" : ", srvAllowedHostsSuffix=" + srvAllowedHostsSuffix)
+ ", mode=" + mode
+ ", requiredClusterType=" + requiredClusterType
+ ", requiredReplicaSetName='" + requiredReplicaSetName + '\''
@@ -624,6 +678,7 @@ private ClusterSettings(final Builder builder) {
srvHost = builder.srvHost;
srvMaxHosts = builder.srvMaxHosts;
srvServiceName = builder.srvServiceName;
+ srvAllowedHostsSuffix = builder.srvAllowedHostsSuffix;
hosts = builder.hosts;
requiredReplicaSetName = builder.requiredReplicaSetName;
if (builder.mode != null) {
diff --git a/driver-core/src/main/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitor.java b/driver-core/src/main/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitor.java
index 9a17f2eb4f9..9d40d7a5469 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitor.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitor.java
@@ -24,6 +24,7 @@
import com.mongodb.internal.diagnostics.logging.Logger;
import com.mongodb.internal.diagnostics.logging.Loggers;
import com.mongodb.internal.dns.DnsResolver;
+import com.mongodb.lang.Nullable;
import java.util.Collections;
import java.util.HashSet;
@@ -38,6 +39,8 @@ class DefaultDnsSrvRecordMonitor implements DnsSrvRecordMonitor {
private final String hostName;
private final String srvServiceName;
+ @Nullable
+ private final String srvAllowedHostsSuffix;
private final long rescanFrequencyMillis;
private final long noRecordsRescanFrequencyMillis;
private final DnsSrvRecordInitializer dnsSrvRecordInitializer;
@@ -45,11 +48,13 @@ class DefaultDnsSrvRecordMonitor implements DnsSrvRecordMonitor {
private final Thread monitorThread;
private volatile boolean isClosed;
- DefaultDnsSrvRecordMonitor(final String hostName, final String srvServiceName, final long rescanFrequencyMillis, final long noRecordsRescanFrequencyMillis,
+ DefaultDnsSrvRecordMonitor(final String hostName, final String srvServiceName, @Nullable final String srvAllowedHostsSuffix,
+ final long rescanFrequencyMillis, final long noRecordsRescanFrequencyMillis,
final DnsSrvRecordInitializer dnsSrvRecordInitializer, final ClusterId clusterId,
final DnsResolver dnsResolver) {
this.hostName = hostName;
this.srvServiceName = srvServiceName;
+ this.srvAllowedHostsSuffix = srvAllowedHostsSuffix;
this.rescanFrequencyMillis = rescanFrequencyMillis;
this.noRecordsRescanFrequencyMillis = noRecordsRescanFrequencyMillis;
this.dnsSrvRecordInitializer = dnsSrvRecordInitializer;
@@ -78,7 +83,8 @@ public void run() {
try {
while (!isClosed && shouldContinueMonitoring()) {
try {
- List resolvedHostNames = dnsResolver.resolveHostFromSrvRecords(hostName, srvServiceName);
+ List resolvedHostNames = dnsResolver.resolveHostFromSrvRecords(hostName, srvServiceName,
+ srvAllowedHostsSuffix);
Set hosts = createServerAddressSet(resolvedHostNames);
if (isClosed) {
diff --git a/driver-core/src/main/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitorFactory.java b/driver-core/src/main/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitorFactory.java
index d303acdef99..75927c52048 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitorFactory.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitorFactory.java
@@ -43,8 +43,9 @@ public DefaultDnsSrvRecordMonitorFactory(final ClusterId clusterId, final Server
}
@Override
- public DnsSrvRecordMonitor create(final String hostName, final String srvServiceName, final DnsSrvRecordInitializer dnsSrvRecordInitializer) {
- return new DefaultDnsSrvRecordMonitor(hostName, srvServiceName, DEFAULT_RESCAN_FREQUENCY_MILLIS, noRecordsRescanFrequency,
- dnsSrvRecordInitializer, clusterId, new DefaultDnsResolver(dnsClient));
+ public DnsSrvRecordMonitor create(final String hostName, final String srvServiceName,
+ @Nullable final String srvAllowedHostsSuffix, final DnsSrvRecordInitializer dnsSrvRecordInitializer) {
+ return new DefaultDnsSrvRecordMonitor(hostName, srvServiceName, srvAllowedHostsSuffix, DEFAULT_RESCAN_FREQUENCY_MILLIS,
+ noRecordsRescanFrequency, dnsSrvRecordInitializer, clusterId, new DefaultDnsResolver(dnsClient));
}
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/DnsMultiServerCluster.java b/driver-core/src/main/com/mongodb/internal/connection/DnsMultiServerCluster.java
index e165146dd29..3fe8c2f381b 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/DnsMultiServerCluster.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/DnsMultiServerCluster.java
@@ -46,6 +46,7 @@ public DnsMultiServerCluster(final ClusterId clusterId, final ClusterSettings se
final DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory) {
super(clusterId, settings, serverFactory, clientMetadata);
dnsSrvRecordMonitor = dnsSrvRecordMonitorFactory.create(assertNotNull(settings.getSrvHost()), settings.getSrvServiceName(),
+ settings.getSrvAllowedHostsSuffix(),
new DnsSrvRecordInitializer() {
private volatile boolean initialized;
diff --git a/driver-core/src/main/com/mongodb/internal/connection/DnsSrvRecordMonitorFactory.java b/driver-core/src/main/com/mongodb/internal/connection/DnsSrvRecordMonitorFactory.java
index 202b490c1e5..632fec00109 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/DnsSrvRecordMonitorFactory.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/DnsSrvRecordMonitorFactory.java
@@ -16,9 +16,12 @@
package com.mongodb.internal.connection;
+import com.mongodb.lang.Nullable;
+
/**
* This class is not part of the public API and may be removed or changed at any time
*/
public interface DnsSrvRecordMonitorFactory {
- DnsSrvRecordMonitor create(String hostName, String srvServiceName, DnsSrvRecordInitializer dnsSrvRecordInitializer);
+ DnsSrvRecordMonitor create(String hostName, String srvServiceName, @Nullable String srvAllowedHostsSuffix,
+ DnsSrvRecordInitializer dnsSrvRecordInitializer);
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java b/driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java
index b64ea64ae24..a5131074ba1 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java
@@ -15,16 +15,81 @@
*/
package com.mongodb.internal.connection;
+import java.net.IDN;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Set;
+import java.util.TreeSet;
import java.util.regex.Pattern;
/**
* This class is not part of the public API and may be removed or changed at any time
*/
public class DomainNameUtils {
- private static final Pattern DOMAIN_PATTERN =
- Pattern.compile("^(?=.{1,255}$)((([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,63}|localhost))$");
+ private static final Pattern DOMAIN_PATTERN = Pattern.compile(
+ "^(?=.{1,255}$)(([a-z0-9]([a-z0-9\\-]{0,61}[a-z0-9])?\\.)*(?![0-9]+$)[a-z0-9]([a-z0-9\\-]{0,61}[a-z0-9])?)$",
+ Pattern.CASE_INSENSITIVE);
+ private static final Set VALID_SINGLE_LABEL_SUFFIXES = Collections.unmodifiableSet(new TreeSet<>(Arrays.asList(
+ // RFC 6761 special use names
+ "test", "localhost", "invalid", "example",
+ // RFC 6762 multicast DNS
+ "local",
+ // reserved by ICANN for private use
+ "internal",
+ // not reserved by ICANN, but commonly used privately
+ "corp", "home", "mail")));
static boolean isDomainName(final String domainName) {
return DOMAIN_PATTERN.matcher(domainName).matches();
}
+
+ /**
+ * Validates and normalizes a {@code srvAllowedHostsSuffix} value for use as the domain in SRV host validation.
+ * A leading {@code "."} is prepended if absent, so the returned value always begins with {@code "."}; this is what
+ * is stored and returned to callers. The suffix must contain at least one non-empty domain label and no
+ * whitespace.
+ *
+ * @param srvAllowedHostsSuffix the non-null suffix to validate
+ * @return the normalized suffix, always beginning with {@code "."}
+ * @throws IllegalArgumentException if the suffix contains whitespace, contains no domain label (it is empty or
+ * consists only of a leading {@code "."}), contains an empty domain label (consecutive {@code "."} characters
+ * or a trailing {@code "."}), or, top-level domain
+ */
+ public static String normalizeSrvAllowedHostsSuffix(final String srvAllowedHostsSuffix) {
+ if (containsWhitespace(srvAllowedHostsSuffix)) {
+ throw new IllegalArgumentException("srvAllowedHostsSuffix must not contain whitespace");
+ }
+ // A single leading '.' is allowed (it is the documented suffix form); everything after it must be one or more
+ // non-empty domain labels.
+ boolean hasLeadingDot = srvAllowedHostsSuffix.startsWith(".");
+ String labels = hasLeadingDot ? srvAllowedHostsSuffix.substring(1) : srvAllowedHostsSuffix;
+ if (labels.endsWith(".")) {
+ labels = labels.substring(0, labels.length() - 1);
+ }
+ if (labels.isEmpty()) {
+ throw new IllegalArgumentException("srvAllowedHostsSuffix must contain at least one domain label");
+ }
+ try {
+ labels = IDN.toASCII(labels, IDN.ALLOW_UNASSIGNED).toLowerCase(Locale.ROOT);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("srvAllowedHostsSuffix is not a valid domain: " + srvAllowedHostsSuffix, e);
+ }
+ if (!isDomainName(labels)) {
+ throw new IllegalArgumentException("srvAllowedHostsSuffix is not a valid domain: " + srvAllowedHostsSuffix);
+ }
+ if (!VALID_SINGLE_LABEL_SUFFIXES.contains(labels) && !labels.contains(".")) {
+ throw new IllegalArgumentException("srvAllowedHostsSuffix must contain at least two domain labels");
+ }
+ return "." + labels;
+ }
+
+ private static boolean containsWhitespace(final String value) {
+ for (int i = 0; i < value.length(); i++) {
+ if (Character.isWhitespace(value.charAt(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/LoadBalancedCluster.java b/driver-core/src/main/com/mongodb/internal/connection/LoadBalancedCluster.java
index 2401a9e014a..26d466d1cf0 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/LoadBalancedCluster.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/LoadBalancedCluster.java
@@ -112,6 +112,7 @@ final class LoadBalancedCluster implements Cluster {
} else {
notNull("dnsSrvRecordMonitorFactory", dnsSrvRecordMonitorFactory);
dnsSrvRecordMonitor = dnsSrvRecordMonitorFactory.create(assertNotNull(settings.getSrvHost()), settings.getSrvServiceName(),
+ settings.getSrvAllowedHostsSuffix(),
new DnsSrvRecordInitializer() {
@Override
diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java
index 9a1a42511e1..21b942894fa 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java
@@ -164,12 +164,14 @@ private void sendConnect(final Timeout timeout) throws IOException {
final int hostLength = bytesOfHost.length;
AddressType addressType;
- byte[] ipAddress = null;
- if (isDomainName(host)) {
+ byte[] ipAddress = InetAddressUtils.ipStringToBytes(host);
+ if (ipAddress != null) {
+ addressType = determineAddressType(ipAddress);
+
+ } else if (isDomainName(host)) {
addressType = DOMAIN_NAME;
} else {
- ipAddress = createByteArrayFromIpAddress(host);
- addressType = determineAddressType(ipAddress);
+ throw new SocketException(host + IP_PARSING_ERROR_SUFFIX);
}
byte[] bufferSent = createBuffer(addressType, hostLength);
bufferSent[0] = SOCKS_VERSION;
@@ -206,14 +208,6 @@ private static void addPort(final byte[] bufferSent, final int index, final int
bufferSent[index + 1] = (byte) port;
}
- private static byte[] createByteArrayFromIpAddress(final String host) throws SocketException {
- byte[] bytes = InetAddressUtils.ipStringToBytes(host);
- if (bytes == null) {
- throw new SocketException(host + IP_PARSING_ERROR_SUFFIX);
- }
- return bytes;
- }
-
private AddressType determineAddressType(final byte[] ipAddress) {
if (ipAddress.length == IP_V4.getLength()) {
return IP_V4;
diff --git a/driver-core/src/main/com/mongodb/internal/dns/DefaultDnsResolver.java b/driver-core/src/main/com/mongodb/internal/dns/DefaultDnsResolver.java
index f7b433b85bd..3aab287ff35 100644
--- a/driver-core/src/main/com/mongodb/internal/dns/DefaultDnsResolver.java
+++ b/driver-core/src/main/com/mongodb/internal/dns/DefaultDnsResolver.java
@@ -66,18 +66,22 @@ The priority and weight are ignored, and we just concatenate the host (after rem
':' in between, as expected by ServerAddress.
*/
@Override
- public List resolveHostFromSrvRecords(final String srvHost, final String srvServiceName) {
- List srvHostParts = asList(srvHost.split("\\."));
-
- String srvHostDomain;
- boolean srvHasLessThanThreeParts = srvHostParts.size() < 3;
- if (srvHasLessThanThreeParts) {
- srvHostDomain = srvHost;
- } else {
- srvHostDomain = srvHost.substring(srvHost.indexOf('.') + 1);
+ public List resolveHostFromSrvRecords(final String srvHost, final String srvServiceName,
+ @Nullable final String srvAllowedHostsSuffix) {
+ // srvAllowedHostsSuffix, when set, is already normalized (it begins with '.') by the configuration layer and
+ // overrides the inferred domain. The leading '.' makes the comparison match on a full domain label boundary
+ // and not a partial label.
+
+ // The inferred-domain checks below are only used when no suffix is configured.
+ boolean srvHasLessThanThreeParts = false;
+ List srvHostDomainParts = null;
+ if (srvAllowedHostsSuffix == null) {
+ List srvHostParts = asList(srvHost.split("\\."));
+ srvHasLessThanThreeParts = srvHostParts.size() < 3;
+ String srvHostDomain = srvHasLessThanThreeParts ? srvHost : srvHost.substring(srvHost.indexOf('.') + 1);
+ srvHostDomainParts = asList(srvHostDomain.split("\\."));
}
- List srvHostDomainParts = asList(srvHostDomain.split("\\."));
List hosts = new ArrayList<>();
String resourceName = "_" + srvServiceName + "._tcp." + srvHost;
try {
@@ -89,27 +93,45 @@ public List resolveHostFromSrvRecords(final String srvHost, final String
for (String srvRecord : srvAttributeValues) {
String[] split = srvRecord.split(" ");
String resolvedHost = split[3].endsWith(".") ? split[3].substring(0, split[3].length() - 1) : split[3];
- String resolvedHostDomain = resolvedHost.substring(resolvedHost.indexOf('.') + 1);
- List resolvedHostDomainParts = asList(resolvedHostDomain.split("\\."));
- if (!sameDomain(srvHostDomainParts, resolvedHostDomainParts)) {
- throw new MongoConfigurationException(
- format("The SRV host name '%s' resolved to a host '%s' that does not share domain name",
- srvHost, resolvedHost));
- }
- if (srvHasLessThanThreeParts && resolvedHostDomainParts.size() <= srvHostDomainParts.size()) {
- throw new MongoConfigurationException(
- format("The SRV host name '%s' resolved to a host '%s' that does not have at least one more domain level",
- srvHost, resolvedHost));
+ if (srvAllowedHostsSuffix != null) {
+ // DNS host names are case-insensitive, so the suffix comparison must be too. The provided name will also be in
+ // Punycode since that's what stored in an SRV record.
+ if (!endsWithIgnoreCase(resolvedHost, srvAllowedHostsSuffix)) {
+ throw new MongoConfigurationException(
+ format("The SRV host name '%s' resolved to a host '%s' that does not end with the "
+ + "configured srvAllowedHostsSuffix '%s'", srvHost, resolvedHost, srvAllowedHostsSuffix));
+ }
+ } else {
+ String resolvedHostDomain = resolvedHost.substring(resolvedHost.indexOf('.') + 1);
+ List resolvedHostDomainParts = asList(resolvedHostDomain.split("\\."));
+ if (!sameDomain(srvHostDomainParts, resolvedHostDomainParts)) {
+ throw new MongoConfigurationException(
+ format("The SRV host name '%s' resolved to a host '%s' that does not share domain name",
+ srvHost, resolvedHost));
+ }
+ if (srvHasLessThanThreeParts && resolvedHostDomainParts.size() <= srvHostDomainParts.size()) {
+ throw new MongoConfigurationException(
+ format("The SRV host name '%s' resolved to a host '%s' that does not have at least one more "
+ + "domain level", srvHost, resolvedHost));
+ }
}
hosts.add(resolvedHost + ":" + split[2]);
}
+ } catch (MongoConfigurationException e) {
+ // Preserve the specific validation message rather than re-wrapping it.
+ throw e;
} catch (Exception e) {
throw new MongoConfigurationException(format("Failed looking up SRV record for '%s'.", resourceName), e);
}
return hosts;
}
+ private static boolean endsWithIgnoreCase(final String value, final String suffix) {
+ return value.length() >= suffix.length()
+ && value.regionMatches(true, value.length() - suffix.length(), suffix, 0, suffix.length());
+ }
+
private static boolean sameDomain(final List srvHostDomainParts, final List resolvedHostDomainParts) {
if (srvHostDomainParts.size() > resolvedHostDomainParts.size()) {
return false;
diff --git a/driver-core/src/main/com/mongodb/internal/dns/DnsResolver.java b/driver-core/src/main/com/mongodb/internal/dns/DnsResolver.java
index 5ff12c33bc8..4354a2b2113 100644
--- a/driver-core/src/main/com/mongodb/internal/dns/DnsResolver.java
+++ b/driver-core/src/main/com/mongodb/internal/dns/DnsResolver.java
@@ -16,6 +16,8 @@
package com.mongodb.internal.dns;
+import com.mongodb.lang.Nullable;
+
import java.util.List;
/**
@@ -25,7 +27,7 @@
*/
public interface DnsResolver {
- List resolveHostFromSrvRecords(String srvHost, String srvServiceName);
+ List resolveHostFromSrvRecords(String srvHost, String srvServiceName, @Nullable String srvAllowedHostsSuffix);
String resolveAdditionalQueryParametersFromTxtRecords(String host);
}
diff --git a/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java b/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java
index 43abfa5b9f7..bd7e10d2ceb 100644
--- a/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java
+++ b/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java
@@ -26,6 +26,7 @@ class DomainNameUtilsTest {
@ParameterizedTest
@ValueSource(strings = {
+ "xn--frosch-6ya.w23",
"hyphen-domain.com",
"sub.domain.com",
"sub.domain.c.com.com",
@@ -51,7 +52,6 @@ void shouldReturnTrueWithValidHostName(final String hostname) {
@ParameterizedTest
@ValueSource(strings = {
- "xn--frosch-6ya.w23",
"-special_chars_$$.net",
"special_chars_$$.net",
"special_chars_$$.123",
@@ -59,9 +59,10 @@ void shouldReturnTrueWithValidHostName(final String hostname) {
"_subdomain..domain.com",
"subdomain..domain._com",
"subdomain..domain.com_",
- "notlocalhost",
"домен.com", //NON-ASCII
"ẞẞ.com", //NON-ASCII
+ "999.999.999.999", // no all numeric TLD
+ "foo.123", // no all numeric TLD
"abcdefghijklmnopqrstuvwxyz0123456789-abcdefghijklmnopqrstuvwxyzl.com", //64 characters label name.
"a.abcdefghijklmnopqrstuvwxyzabcdefghjklabcdefghijklmnopqrstuvwxyza", //64 characters TLD.
"this-domain-is-really-long-because-it-just-keeps-going-and-going-and-its-still-not-done-yet-because-theres-more.net",
diff --git a/driver-core/src/test/unit/com/mongodb/AbstractConnectionStringTest.java b/driver-core/src/test/unit/com/mongodb/AbstractConnectionStringTest.java
index 4be89f4d9ad..f05be4a0450 100644
--- a/driver-core/src/test/unit/com/mongodb/AbstractConnectionStringTest.java
+++ b/driver-core/src/test/unit/com/mongodb/AbstractConnectionStringTest.java
@@ -238,6 +238,9 @@ protected void testValidOptions() {
} else if (option.getKey().equalsIgnoreCase("srvservicename")) {
String expected = option.getValue().asString().getValue();
assertEquals(expected, connectionString.getSrvServiceName());
+ } else if (option.getKey().equalsIgnoreCase("srvallowedhostssuffix")) {
+ String expected = option.getValue().asString().getValue();
+ assertEquals(expected, connectionString.getSrvAllowedHostsSuffix());
} else {
fail(String.format("Unsupported option '%s' in '%s'", option.getKey(), input));
}
diff --git a/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java b/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java
index e0803bee6eb..e54cc79b507 100644
--- a/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java
+++ b/driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java
@@ -92,6 +92,13 @@ private static String encode(final String string) {
}
}
+ @Test
+ void shouldNotBeEqualWhenSrvAllowedHostsSuffixDiffers() {
+ ConnectionString first = new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=.build.10gen.cc");
+ ConnectionString second = new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=.other.10gen.cc");
+ assertNotEquals(first, second);
+ }
+
@Test
void serverMonitoringMode() {
assertAll(
@@ -140,4 +147,153 @@ void enableOverloadRetargeting() {
() -> assertNull(new ConnectionString(DEFAULT_OPTIONS + "enableOverloadRetargeting=foos").getEnableOverloadRetargeting())
);
}
+
+ @Test
+ void shouldParseSrvAllowedHostsSuffixWithLeadingDot() {
+ assertEquals(".build.10gen.cc",
+ new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=.build.10gen.cc")
+ .getSrvAllowedHostsSuffix());
+ }
+
+ @Test
+ void shouldNormalizeSrvAllowedHostsSuffixWithoutLeadingDot() {
+ // a leading "." is prepended at parse time, so the stored/returned value always begins with "."
+ assertEquals(".build.10gen.cc",
+ new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=build.10gen.cc")
+ .getSrvAllowedHostsSuffix());
+ }
+
+ @Test
+ void shouldDefaultSrvAllowedHostsSuffixToNull() {
+ assertNull(new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/").getSrvAllowedHostsSuffix());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixUsedWithNonSrvUri() {
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> new ConnectionString("mongodb://localhost/?srvAllowedHostsSuffix=.build.10gen.cc"));
+ assertEquals("srvAllowedHostsSuffix can only be specified with mongodb+srv protocol", e.getMessage());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixIsEmpty() {
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix="));
+ assertEquals("srvAllowedHostsSuffix must contain at least one domain label", e.getMessage());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixIsOnlyADot() {
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=."));
+ assertEquals("srvAllowedHostsSuffix must contain at least one domain label", e.getMessage());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixIsOnlyDots() {
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=.."));
+ assertEquals("srvAllowedHostsSuffix must contain at least one domain label", e.getMessage());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixHasLeadingDoubleDot() {
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=..build.10gen.cc"));
+ assertEquals("srvAllowedHostsSuffix is not a valid domain: ..build.10gen.cc", e.getMessage());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixHasEmptyInteriorLabel() {
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=build..10gen.cc"));
+ assertEquals("srvAllowedHostsSuffix is not a valid domain: build..10gen.cc", e.getMessage());
+ }
+
+ @Test
+ void shouldNormalizeSrvAllowedHostsSuffixWhenHasTrailingDot() {
+ assertEquals(
+ ".build.10gen.cc",
+ new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=.build.10gen.cc.")
+ .getSrvAllowedHostsSuffix());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixContainsWhitespace() {
+ IllegalArgumentException e = assertThrows(
+ IllegalArgumentException.class,
+ () -> new ConnectionString(
+ "mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=.build.10gen.cc%20"));
+ assertEquals("srvAllowedHostsSuffix must not contain whitespace", e.getMessage());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixIsNotDomain() {
+ IllegalArgumentException e = assertThrows(
+ IllegalArgumentException.class,
+ () -> new ConnectionString(
+ "mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=.*.10gen.cc"));
+ assertEquals("srvAllowedHostsSuffix is not a valid domain: .*.10gen.cc", e.getMessage());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixIsTopLevelDomain() {
+ IllegalArgumentException e = assertThrows(
+ IllegalArgumentException.class,
+ () -> new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=.cc"));
+ assertEquals("srvAllowedHostsSuffix must contain at least two domain labels", e.getMessage());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixIsTopLevelDomainWildcard() {
+ // *.ck is disallowed, but www.ck is permitted by exception
+ IllegalArgumentException e = assertThrows(
+ IllegalArgumentException.class,
+ () -> new ConnectionString("mongodb+srv://test12.10gen.ck/?srvAllowedHostsSuffix=.ck"));
+ assertEquals("srvAllowedHostsSuffix must contain at least two domain labels", e.getMessage());
+ }
+
+ @Test
+ void allowSrvAllowedHostsSuffixWithExemptWildcard() {
+ // *.ck is disallowed, but www.ck is permitted by exception
+ assertEquals(
+ ".www.ck",
+ new ConnectionString("mongodb+srv://test12.www.ck/?srvAllowedHostsSuffix=www.ck")
+ .getSrvAllowedHostsSuffix());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixIsTopLevelDomainUnicode() {
+ IllegalArgumentException e = assertThrows(
+ IllegalArgumentException.class,
+ () -> new ConnectionString(
+ "mongodb+srv://test12.test.build.10gen.xn--3pxu8k/?srvAllowedHostsSuffix=.点看"));
+ assertEquals("srvAllowedHostsSuffix must contain at least two domain labels", e.getMessage());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixIsTopLevelDomainPunycode() {
+ IllegalArgumentException e = assertThrows(
+ IllegalArgumentException.class,
+ () -> new ConnectionString(
+ "mongodb+srv://test12.test.build.10gen.xn--3pxu8k/?srvAllowedHostsSuffix=.xn--3pxu8k"));
+ assertEquals("srvAllowedHostsSuffix must contain at least two domain labels", e.getMessage());
+ }
+
+ @Test
+ void shouldThrowWhenSrvAllowedHostsSuffixIsSingleElement() {
+ IllegalArgumentException e = assertThrows(
+ IllegalArgumentException.class,
+ () -> new ConnectionString("mongodb+srv://test12.10gen.ck/?srvAllowedHostsSuffix=.whatever"));
+ assertEquals("srvAllowedHostsSuffix must contain at least two domain labels", e.getMessage());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"test", "localhost", "invalid", "example", "local", "internal", "corp", "home", "mail"})
+ void allowSrvAllowedHostsSuffixSpecialSingleLabel(final String suffix) {
+ assertEquals(
+ "." + suffix,
+ new ConnectionString("mongodb+srv://test12.www.ck/?srvAllowedHostsSuffix=" + suffix)
+ .getSrvAllowedHostsSuffix());
+ }
}
diff --git a/driver-core/src/test/unit/com/mongodb/UriOptionsTest.java b/driver-core/src/test/unit/com/mongodb/UriOptionsTest.java
index b6f46b1b014..11834fd1b97 100644
--- a/driver-core/src/test/unit/com/mongodb/UriOptionsTest.java
+++ b/driver-core/src/test/unit/com/mongodb/UriOptionsTest.java
@@ -30,9 +30,6 @@
public class UriOptionsTest extends AbstractConnectionStringTest {
public UriOptionsTest(final String filename, final String description, final String input, final BsonDocument definition) {
super(filename, description, input, definition);
- // TODO-JAVA-6235 https://jira.mongodb.org/browse/JAVA-6235
- assumeFalse(filename.equals("srv-options.json"));
-
}
@Test
diff --git a/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy b/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy
index 36da5c61e2d..aa76abca093 100644
--- a/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy
@@ -107,6 +107,7 @@ class ClusterSettingsSpecification extends Specification {
.hosts([new ServerAddress('localhost')])
.srvMaxHosts(4)
.srvServiceName('foo')
+ .srvAllowedHostsSuffix('.build.10gen.cc')
.build()
expect:
@@ -114,6 +115,81 @@ class ClusterSettingsSpecification extends Specification {
ClusterSettings.builder(customSettings).applySettings(defaultSettings).build() == defaultSettings
}
+ def 'should set srvAllowedHostsSuffix'() {
+ when:
+ def settings = ClusterSettings.builder().srvHost('test12.test.build.10gen.cc')
+ .srvAllowedHostsSuffix('.build.10gen.cc').build()
+
+ then:
+ settings.srvAllowedHostsSuffix == '.build.10gen.cc'
+ }
+
+ def 'should normalize srvAllowedHostsSuffix with trailing dot'() {
+ when:
+ def settings = ClusterSettings.builder().srvHost('test12.test.build.10gen.cc')
+ .srvAllowedHostsSuffix('.build.10gen.cc.').build()
+
+ then:
+ settings.srvAllowedHostsSuffix == '.build.10gen.cc'
+ }
+
+ def 'should normalize srvAllowedHostsSuffix without a leading dot'() {
+ when:
+ def settings = ClusterSettings.builder().srvHost('test12.test.build.10gen.cc')
+ .srvAllowedHostsSuffix('build.10gen.cc').build()
+
+ then:
+ settings.srvAllowedHostsSuffix == '.build.10gen.cc'
+ }
+
+ def 'should default srvAllowedHostsSuffix to null'() {
+ expect:
+ ClusterSettings.builder().hosts([new ServerAddress()]).build().srvAllowedHostsSuffix == null
+ }
+
+ def 'should apply srvAllowedHostsSuffix from connection string'() {
+ when:
+ def settings = ClusterSettings.builder().applyConnectionString(
+ new ConnectionString('mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=.build.10gen.cc')).build()
+
+ then:
+ settings.srvAllowedHostsSuffix == '.build.10gen.cc'
+ }
+
+ def 'should apply srvAllowedHostsSuffix from connection string when loadBalanced'() {
+ when:
+ def settings = ClusterSettings.builder().applyConnectionString(
+ new ConnectionString('mongodb+srv://test12.test.build.10gen.cc/'
+ + '?loadBalanced=true&srvAllowedHostsSuffix=.build.10gen.cc')).build()
+
+ then:
+ settings.mode == ClusterConnectionMode.LOAD_BALANCED
+ settings.srvAllowedHostsSuffix == '.build.10gen.cc'
+ }
+
+ def 'should apply srvServiceName from connection string when loadBalanced'() {
+ when:
+ def settings = ClusterSettings.builder().applyConnectionString(
+ new ConnectionString('mongodb+srv://test12.test.build.10gen.cc/'
+ + '?loadBalanced=true&srvServiceName=customname')).build()
+
+ then:
+ settings.mode == ClusterConnectionMode.LOAD_BALANCED
+ settings.srvServiceName == 'customname'
+ }
+
+ def 'should throw IllegalArgumentException for invalid srvAllowedHostsSuffix'() {
+ when:
+ ClusterSettings.builder().srvHost('test12.test.build.10gen.cc').srvAllowedHostsSuffix(suffix)
+
+ then:
+ thrown(IllegalArgumentException)
+
+ where:
+ suffix << ['', ' ', '.', '..', '...', ' .build.10gen.cc', '.build.10gen.cc ',
+ '..build.10gen.cc', 'build..10gen.cc']
+ }
+
def 'when hosts contains more than one element and mode is SINGLE, should throw IllegalArgumentException'() {
when:
def builder = ClusterSettings.builder()
diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitorSpecification.groovy
index 4839fa53024..f489024d88d 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitorSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitorSpecification.groovy
@@ -37,9 +37,9 @@ class DefaultDnsSrvRecordMonitorSpecification extends Specification {
def expectedResolvedHosts = [resolvedHostOne, resolvedHostTwo]
def dnsSrvRecordInitializer = new TestDnsSrvRecordInitializer(ClusterType.REPLICA_SET, 1)
def dnsResolver = Mock(DnsResolver) {
- 1 * resolveHostFromSrvRecords(hostName, srvServiceName) >> expectedResolvedHosts
+ 1 * resolveHostFromSrvRecords(hostName, srvServiceName, null) >> expectedResolvedHosts
}
- def monitor = new DefaultDnsSrvRecordMonitor(hostName, srvServiceName, 1, 10000, dnsSrvRecordInitializer, new ClusterId(),
+ def monitor = new DefaultDnsSrvRecordMonitor(hostName, srvServiceName, null, 1, 10000, dnsSrvRecordInitializer, new ClusterId(),
dnsResolver)
when:
@@ -64,9 +64,10 @@ class DefaultDnsSrvRecordMonitorSpecification extends Specification {
def expectedResolvedHostsTwo = [resolvedHostTwo, resolvedHostThree]
def dnsSrvRecordInitializer = new TestDnsSrvRecordInitializer(ClusterType.SHARDED, 2)
def dnsResolver = Mock(DnsResolver) {
- _ * resolveHostFromSrvRecords(hostName, srvServiceName) >>> [expectedResolvedHostsOne, expectedResolvedHostsTwo]
+ _ * resolveHostFromSrvRecords(hostName, srvServiceName, null) >>> [expectedResolvedHostsOne, expectedResolvedHostsTwo]
}
- def monitor = new DefaultDnsSrvRecordMonitor(hostName, srvServiceName, 1, 1, dnsSrvRecordInitializer, new ClusterId(), dnsResolver)
+ def monitor = new DefaultDnsSrvRecordMonitor(hostName, srvServiceName, null, 1, 1, dnsSrvRecordInitializer, new ClusterId(),
+ dnsResolver)
when:
monitor.start()
@@ -86,11 +87,11 @@ class DefaultDnsSrvRecordMonitorSpecification extends Specification {
def srvServiceName = 'mongodb'
def dnsSrvRecordInitializer = new TestDnsSrvRecordInitializer(ClusterType.UNKNOWN, 1)
def dnsResolver = Mock(DnsResolver) {
- _ * resolveHostFromSrvRecords(hostName, srvServiceName) >> {
+ _ * resolveHostFromSrvRecords(hostName, srvServiceName, null) >> {
throw initializationException
}
}
- def monitor = new DefaultDnsSrvRecordMonitor(hostName, srvServiceName, 1, 10000, dnsSrvRecordInitializer, new ClusterId(),
+ def monitor = new DefaultDnsSrvRecordMonitor(hostName, srvServiceName, null, 1, 10000, dnsSrvRecordInitializer, new ClusterId(),
dnsResolver)
when:
@@ -118,10 +119,10 @@ class DefaultDnsSrvRecordMonitorSpecification extends Specification {
def resolvedHostListTwo = ['localhost.test.build.10gen.cc:27018']
def dnsSrvRecordInitializer = new TestDnsSrvRecordInitializer(ClusterType.SHARDED, 2)
def dnsResolver = Mock(DnsResolver) {
- _ * resolveHostFromSrvRecords(hostName, srvServiceName) >> resolvedHostListOne >> {
+ _ * resolveHostFromSrvRecords(hostName, srvServiceName, null) >> resolvedHostListOne >> {
throw initializationException } >> resolvedHostListTwo
}
- def monitor = new DefaultDnsSrvRecordMonitor(hostName, srvServiceName, 1, 1,
+ def monitor = new DefaultDnsSrvRecordMonitor(hostName, srvServiceName, null, 1, 1,
dnsSrvRecordInitializer, new ClusterId(), dnsResolver)
when:
diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/DnsMultiServerClusterSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/connection/DnsMultiServerClusterSpecification.groovy
index 930e30b2c7b..f4dac72b4d5 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/connection/DnsMultiServerClusterSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/internal/connection/DnsMultiServerClusterSpecification.groovy
@@ -55,7 +55,8 @@ class DnsMultiServerClusterSpecification extends Specification {
DnsSrvRecordInitializer initializer
def dnsSrvRecordMonitorFactory = new DnsSrvRecordMonitorFactory() {
@Override
- DnsSrvRecordMonitor create(final String hostName, String srvServiceName, final DnsSrvRecordInitializer dnsSrvRecordListener) {
+ DnsSrvRecordMonitor create(final String hostName, String srvServiceName, final String srvAllowedHostsSuffix,
+ final DnsSrvRecordInitializer dnsSrvRecordListener) {
initializer = dnsSrvRecordListener
dnsSrvRecordMonitor
}
diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/InitialDnsSeedListDiscoveryProseTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/InitialDnsSeedListDiscoveryProseTest.java
index d49f67a1e38..c5248ba0863 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/connection/InitialDnsSeedListDiscoveryProseTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/connection/InitialDnsSeedListDiscoveryProseTest.java
@@ -26,6 +26,7 @@
import com.mongodb.connection.ServerSettings;
import com.mongodb.internal.dns.DefaultDnsResolver;
import com.mongodb.internal.dns.DnsResolver;
+import com.mongodb.lang.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
@@ -96,21 +97,48 @@ void testThrowWhenReturnAddressDoesnotContainSharedPartOfDomain(final String srv
doTest(srvHost, resolvedHost, true);
}
+ @ParameterizedTest(name = "mongodb+srv://{0} (suffix {2}) => {1}, throws={3}")
+ @CsvSource({
+ // resolved host shares the configured suffix -> valid
+ "test12.test.build.10gen.cc, localhost.build.10gen.cc, .build.10gen.cc, false",
+ // suffix without leading dot is normalized before validation -> valid
+ "test12.test.build.10gen.cc, localhost.build.10gen.cc, build.10gen.cc, false",
+ // resolved host does not end with the configured suffix -> invalid
+ "test12.test.build.10gen.cc, localhost.build.10gen.cc, test.build.10gen.cc, true",
+ // partial-label match must fail because '.' is prepended ('uild.10gen.cc' -> '.uild.10gen.cc')
+ "test12.test.build.10gen.cc, localhost.build.10gen.cc, uild.10gen.cc, true"
+ })
+ @DisplayName("5. srvAllowedHostsSuffix overrides the inferred domain for validation")
+ void testSrvAllowedHostsSuffixValidation(final String srvHost, final String resolvedHost,
+ final String srvAllowedHostsSuffix, final boolean throwException) {
+ doTest(srvHost, resolvedHost, srvAllowedHostsSuffix, throwException);
+ }
+
private void doTest(final String srvHost, final String resolvedHost, final boolean throwException) {
+ doTest(srvHost, resolvedHost, null, throwException);
+ }
+
+ private void doTest(final String srvHost, final String resolvedHost, @Nullable final String srvAllowedHostsSuffix,
+ final boolean throwException) {
final ClusterId clusterId = new ClusterId();
final DnsResolver dnsResolver = new DefaultDnsResolver((name, type) -> singletonList(String.format("10 5 27017 %s",
resolvedHost)));
final DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(dnsSrvRecordMonitorFactory.create(eq(srvHost), eq(SRV_SERVICE_NAME), any(DnsSrvRecordInitializer.class))).thenAnswer(
- invocation -> new DefaultDnsSrvRecordMonitor(srvHost, SRV_SERVICE_NAME, 10, 10,
- invocation.getArgument(2), clusterId, dnsResolver));
+ // The suffix is normalized by ClusterSettings, so match any value and pass through whatever the cluster sends.
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHost), eq(SRV_SERVICE_NAME), any(),
+ any(DnsSrvRecordInitializer.class))).thenAnswer(
+ invocation -> new DefaultDnsSrvRecordMonitor(srvHost, SRV_SERVICE_NAME, invocation.getArgument(2), 10, 10,
+ invocation.getArgument(3), clusterId, dnsResolver));
final ClusterSettings.Builder settingsBuilder = ClusterSettings.builder()
.mode(ClusterConnectionMode.MULTIPLE)
.requiredClusterType(ClusterType.SHARDED)
.srvHost(srvHost);
+ if (srvAllowedHostsSuffix != null) {
+ settingsBuilder.srvAllowedHostsSuffix(srvAllowedHostsSuffix);
+ }
final ClusterableServerFactory serverFactory = mock(ClusterableServerFactory.class);
when(serverFactory.getSettings()).thenReturn(ServerSettings.builder().build());
diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/LoadBalancedClusterTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/LoadBalancedClusterTest.java
index 87dd1581045..c868a46ef96 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/connection/LoadBalancedClusterTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/connection/LoadBalancedClusterTest.java
@@ -125,8 +125,8 @@ public void shouldSelectServerWhenThereIsSRVLookup() {
ClusterableServerFactory serverFactory = mockServerFactory(resolvedServerAddress, expectedServer);
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any())).thenAnswer(
- invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(2)));
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any(), any())).thenAnswer(
+ invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(3)));
cluster = new LoadBalancedCluster(new ClusterId(), clusterSettings, serverFactory, CLIENT_METADATA, dnsSrvRecordMonitorFactory);
@@ -152,8 +152,8 @@ public void shouldSelectServerAsynchronouslyWhenThereIsSRVLookup() {
ClusterableServerFactory serverFactory = mockServerFactory(resolvedServerAddress, expectedServer);
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any())).thenAnswer(
- invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(2)));
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any(), any())).thenAnswer(
+ invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(3)));
cluster = new LoadBalancedCluster(new ClusterId(), clusterSettings, serverFactory, CLIENT_METADATA, dnsSrvRecordMonitorFactory);
@@ -178,8 +178,8 @@ public void shouldFailSelectServerWhenThereIsSRVMisconfiguration() {
ClusterableServerFactory serverFactory = mockServerFactory();
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any())).thenAnswer(
- invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(2))
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any(), any())).thenAnswer(
+ invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(3))
.hosts(Arrays.asList(new ServerAddress("host1"), new ServerAddress("host2"))));
cluster = new LoadBalancedCluster(new ClusterId(), clusterSettings, serverFactory, CLIENT_METADATA, dnsSrvRecordMonitorFactory);
@@ -202,8 +202,8 @@ public void shouldFailSelectServerAsynchronouslyWhenThereIsSRVMisconfiguration()
ClusterableServerFactory serverFactory = mockServerFactory();
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any())).thenAnswer(
- invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(2))
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any(), any())).thenAnswer(
+ invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(3))
.hosts(Arrays.asList(new ServerAddress("host1"), new ServerAddress("host2"))));
cluster = new LoadBalancedCluster(new ClusterId(), clusterSettings, serverFactory, CLIENT_METADATA, dnsSrvRecordMonitorFactory);
@@ -231,8 +231,8 @@ public void shouldTimeoutSelectServerWhenThereIsSRVLookup() {
ClusterableServerFactory serverFactory = mockServerFactory(resolvedServerAddress, expectedServer);
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any())).thenAnswer(
- invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(2)).sleepTime(Duration.ofHours(1)));
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any(), any())).thenAnswer(
+ invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(3)).sleepTime(Duration.ofHours(1)));
cluster = new LoadBalancedCluster(new ClusterId(), clusterSettings, serverFactory, CLIENT_METADATA, dnsSrvRecordMonitorFactory);
@@ -256,8 +256,8 @@ public void shouldTimeoutSelectServerWhenThereIsSRVLookupAndTimeoutMsIsSet() {
ClusterableServerFactory serverFactory = mockServerFactory(resolvedServerAddress, expectedServer);
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any())).thenAnswer(
- invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(2)).sleepTime(Duration.ofHours(1)));
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any(), any())).thenAnswer(
+ invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(3)).sleepTime(Duration.ofHours(1)));
cluster = new LoadBalancedCluster(new ClusterId(), clusterSettings, serverFactory, CLIENT_METADATA, dnsSrvRecordMonitorFactory);
@@ -282,8 +282,8 @@ public void shouldTimeoutSelectServerWhenThereIsSRVLookupException() {
ClusterableServerFactory serverFactory = mockServerFactory(resolvedServerAddress, expectedServer);
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any())).thenAnswer(
- invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(2))
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any(), any())).thenAnswer(
+ invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(3))
.sleepTime(Duration.ofMillis(1))
.exception(new MongoConfigurationException("Unable to resolve SRV record")));
cluster = new LoadBalancedCluster(new ClusterId(), clusterSettings, serverFactory, CLIENT_METADATA, dnsSrvRecordMonitorFactory);
@@ -311,8 +311,8 @@ public void shouldTimeoutSelectServerAsynchronouslyWhenThereIsSRVLookup() {
ClusterableServerFactory serverFactory = mockServerFactory(resolvedServerAddress, expectedServer);
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any())).thenAnswer(
- invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(2)).sleepTime(Duration.ofHours(1)));
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any(), any())).thenAnswer(
+ invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(3)).sleepTime(Duration.ofHours(1)));
cluster = new LoadBalancedCluster(new ClusterId(), clusterSettings, serverFactory, CLIENT_METADATA, dnsSrvRecordMonitorFactory);
@@ -339,8 +339,8 @@ public void shouldTimeoutSelectServerAsynchronouslyWhenThereIsSRVLookupException
ClusterableServerFactory serverFactory = mockServerFactory(resolvedServerAddress, expectedServer);
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any())).thenAnswer(
- invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(2))
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any(), any())).thenAnswer(
+ invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(3))
.sleepTime(Duration.ofMillis(1))
.exception(new MongoConfigurationException("Unable to resolve SRV record")));
cluster = new LoadBalancedCluster(new ClusterId(), clusterSettings, serverFactory, CLIENT_METADATA, dnsSrvRecordMonitorFactory);
@@ -361,11 +361,11 @@ void shouldNotInitServerAfterClosing() {
ClusterableServerFactory serverFactory = mock(ClusterableServerFactory.class);
when(serverFactory.getSettings()).thenReturn(mock(ServerSettings.class));
DnsSrvRecordMonitorFactory srvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(srvRecordMonitorFactory.create(any(), eq(clusterSettings.getSrvServiceName()), any(DnsSrvRecordInitializer.class))).thenReturn(mock(DnsSrvRecordMonitor.class));
+ when(srvRecordMonitorFactory.create(any(), eq(clusterSettings.getSrvServiceName()), any(), any(DnsSrvRecordInitializer.class))).thenReturn(mock(DnsSrvRecordMonitor.class));
ArgumentCaptor serverInitializerCaptor = ArgumentCaptor.forClass(DnsSrvRecordInitializer.class);
// create `cluster` and capture its `DnsSrvRecordInitializer` (server initializer)
LoadBalancedCluster cluster = new LoadBalancedCluster(new ClusterId(), clusterSettings, serverFactory, CLIENT_METADATA, srvRecordMonitorFactory);
- verify(srvRecordMonitorFactory, times(1)).create(any(), eq(clusterSettings.getSrvServiceName()), serverInitializerCaptor.capture());
+ verify(srvRecordMonitorFactory, times(1)).create(any(), eq(clusterSettings.getSrvServiceName()), any(), serverInitializerCaptor.capture());
// close `cluster`, call `DnsSrvRecordInitializer.initialize` and check that it does not result in creating a `ClusterableServer`
cluster.close();
serverInitializerCaptor.getValue().initialize(Collections.singleton(new ServerAddress()));
@@ -405,8 +405,8 @@ public void synchronousConcurrentTest() throws InterruptedException, ExecutionEx
Duration srvResolutionTime = Duration.ofSeconds(5);
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any())).thenAnswer(
- invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(2)).sleepTime(srvResolutionTime));
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any(), any())).thenAnswer(
+ invocation -> new TestDnsSrvRecordMonitor(invocation.getArgument(3)).sleepTime(srvResolutionTime));
cluster = new LoadBalancedCluster(new ClusterId(), clusterSettings, serverFactory, CLIENT_METADATA, dnsSrvRecordMonitorFactory);
int numThreads = 100;
@@ -456,9 +456,9 @@ public void asynchronousConcurrentTest() throws InterruptedException, ExecutionE
Duration srvResolutionTime = Duration.ofSeconds(5);
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
AtomicReference dnsSrvRecordMonitorReference = new AtomicReference<>();
- when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any())).thenAnswer(
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHostName), eq(clusterSettings.getSrvServiceName()), any(), any())).thenAnswer(
invocation -> {
- TestDnsSrvRecordMonitor dnsSrvRecordMonitor = new TestDnsSrvRecordMonitor(invocation.getArgument(2))
+ TestDnsSrvRecordMonitor dnsSrvRecordMonitor = new TestDnsSrvRecordMonitor(invocation.getArgument(3))
.sleepTime(srvResolutionTime);
dnsSrvRecordMonitorReference.set(dnsSrvRecordMonitor);
return dnsSrvRecordMonitor;
diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/SrvPollingProseTests.java b/driver-core/src/test/unit/com/mongodb/internal/connection/SrvPollingProseTests.java
index 51cc4884f02..60f8c31cf4a 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/connection/SrvPollingProseTests.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/connection/SrvPollingProseTests.java
@@ -193,10 +193,10 @@ private void initCluster(final RuntimeException lastResponseException) {
private void initCluster(final TestDnsResolver dnsResolver, @Nullable final Integer srvMaxHosts) {
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class);
- when(dnsSrvRecordMonitorFactory.create(eq(srvHost), eq(srvServiceName), any())).thenAnswer(
+ when(dnsSrvRecordMonitorFactory.create(eq(srvHost), eq(srvServiceName), any(), any())).thenAnswer(
invocation -> {
- dnsSrvRecordMonitor = new DefaultDnsSrvRecordMonitor(srvHost, srvServiceName, 10, 10,
- invocation.getArgument(2), clusterId, dnsResolver);
+ dnsSrvRecordMonitor = new DefaultDnsSrvRecordMonitor(srvHost, srvServiceName, null, 10, 10,
+ invocation.getArgument(3), clusterId, dnsResolver);
return dnsSrvRecordMonitor;
});
cluster = new DnsMultiServerCluster(clusterId, settingsBuilder.srvMaxHosts(srvMaxHosts).build(), serverFactory, CLIENT_METADATA,
@@ -223,7 +223,8 @@ private static final class TestDnsResolver implements DnsResolver {
}
@Override
- public List resolveHostFromSrvRecords(final String srvHost, final String srvServiceName) {
+ public List resolveHostFromSrvRecords(final String srvHost, final String srvServiceName,
+ @Nullable final String srvAllowedHostsSuffix) {
List retVal;
if (curPos >= responses.size() && lastResponseException != null) {
throw lastResponseException;
diff --git a/driver-core/src/test/unit/com/mongodb/internal/dns/DefaultDnsResolverTest.java b/driver-core/src/test/unit/com/mongodb/internal/dns/DefaultDnsResolverTest.java
index b300df8e0ac..e695a73cf89 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/dns/DefaultDnsResolverTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/dns/DefaultDnsResolverTest.java
@@ -17,18 +17,37 @@
package com.mongodb.internal.dns;
import com.mongodb.MongoConfigurationException;
+import com.mongodb.spi.dns.DnsClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import javax.naming.Context;
+import java.util.ArrayList;
+import java.util.List;
+import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class DefaultDnsResolverTest {
private static final String TEST_HOST = "test1.test.build.10gen.cc";
private static final String DEFAULT_PROVIDER_URL_VALUE = System.getProperty(Context.PROVIDER_URL);
+ private static DefaultDnsResolver resolverReturning(final String... srvTargets) {
+ DnsClient dnsClient = (name, type) -> {
+ assertEquals("_mongodb._tcp." + TEST_HOST, name);
+ assertEquals("SRV", type);
+ List records = new ArrayList<>();
+ for (String target : srvTargets) {
+ records.add("10 5 27017 " + target);
+ }
+ return records;
+ };
+ return new DefaultDnsResolver(dnsClient);
+ }
+
@AfterEach
public void resetDefaultProviderUrl() {
if (DEFAULT_PROVIDER_URL_VALUE != null) {
@@ -39,12 +58,51 @@ public void resetDefaultProviderUrl() {
@Test
public void nonDnsProviderUrlShouldBeIgnored() {
System.setProperty(Context.PROVIDER_URL, "file:///tmp/provider.txt");
- assertDoesNotThrow(() -> new DefaultDnsResolver().resolveHostFromSrvRecords(TEST_HOST, "mongodb"));
+ assertDoesNotThrow(() -> new DefaultDnsResolver().resolveHostFromSrvRecords(TEST_HOST, "mongodb", null));
}
@Test
public void dnsProviderUrlShouldNotBeIgnored() {
System.setProperty(Context.PROVIDER_URL, "dns:///mongodb.unknown.server.com");
- assertThrows(MongoConfigurationException.class, () -> new DefaultDnsResolver().resolveHostFromSrvRecords(TEST_HOST, "mongodb"));
+ assertThrows(MongoConfigurationException.class, () -> new DefaultDnsResolver().resolveHostFromSrvRecords(TEST_HOST, "mongodb", null));
+ }
+
+ @Test
+ public void shouldAcceptResolvedHostEndingWithSrvAllowedHostsSuffix() {
+ DefaultDnsResolver resolver = resolverReturning("localhost.build.10gen.cc.");
+ assertEquals(singletonList("localhost.build.10gen.cc:27017"),
+ resolver.resolveHostFromSrvRecords(TEST_HOST, "mongodb", ".build.10gen.cc"));
+ }
+
+
+ @Test
+ public void shouldMatchSrvAllowedHostsSuffixCaseInsensitively() {
+ DefaultDnsResolver resolver = resolverReturning("LOCALHOST.Build.10GEN.cc.");
+ assertEquals(singletonList("LOCALHOST.Build.10GEN.cc:27017"),
+ resolver.resolveHostFromSrvRecords(TEST_HOST, "mongodb", ".build.10gen.cc"));
+ }
+
+ @Test
+ public void shouldThrowWhenResolvedHostDoesNotEndWithSrvAllowedHostsSuffix() {
+ DefaultDnsResolver resolver = resolverReturning("localhost.build.10gen.cc.");
+ MongoConfigurationException e = assertThrows(MongoConfigurationException.class,
+ () -> resolver.resolveHostFromSrvRecords(TEST_HOST, "mongodb", ".test.build.10gen.cc"));
+ assertTrue(e.getMessage().contains("srvAllowedHostsSuffix"));
+ }
+
+ @Test
+ public void shouldThrowWhenAnyResolvedHostDoesNotEndWithSrvAllowedHostsSuffix() {
+ DefaultDnsResolver resolver = resolverReturning("ok.build.10gen.cc.", "bad.evil.example.com.");
+ MongoConfigurationException e = assertThrows(MongoConfigurationException.class,
+ () -> resolver.resolveHostFromSrvRecords(TEST_HOST, "mongodb", ".build.10gen.cc"));
+ assertTrue(e.getMessage().contains("bad.evil.example.com"));
+ }
+
+ @Test
+ public void shouldThrowWhenResolvedHostEqualsSrvAllowedHostsSuffixWithoutAdditionalLabel() {
+ // a leading '.' is prepended, so a bare host equal to the suffix has no label boundary and must be rejected
+ DefaultDnsResolver resolver = resolverReturning("build.10gen.cc.");
+ assertThrows(MongoConfigurationException.class,
+ () -> resolver.resolveHostFromSrvRecords(TEST_HOST, "mongodb", ".build.10gen.cc"));
}
}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/InitialDnsSeedlistDiscoveryTest.java b/driver-sync/src/test/functional/com/mongodb/client/InitialDnsSeedlistDiscoveryTest.java
index 12146540dbb..59015fb7714 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/InitialDnsSeedlistDiscoveryTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/InitialDnsSeedlistDiscoveryTest.java
@@ -42,7 +42,6 @@
import util.JsonPoweredTestHelper;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -97,21 +96,6 @@ public InitialDnsSeedlistDiscoveryTest(final String filename, final String paren
this.isError = isError;
this.options = options;
this.executePingCommand = executePingCommand;
-
- // TODO-JAVA-6235 https://jira.mongodb.org/browse/JAVA-6235
- assumeFalse(Arrays.asList(
- "srvAllowedHostsSuffix-case-insensitive.json",
- "srvAllowedHostsSuffix-mismatch.json",
- "srvAllowedHostsSuffix-period-only.json",
- "srvAllowedHostsSuffix-psl-not-public-suffix.json",
- "srvAllowedHostsSuffix-psl-public-suffix-capitalized.json",
- "srvAllowedHostsSuffix-psl-public-suffix.json",
- "srvAllowedHostsSuffix-tld-only.json",
- "srvAllowedHostsSuffix-trailing-dot.json",
- "srvAllowedHostsSuffix-with_dot.json",
- "srvAllowedHostsSuffix-without_dot_fail.json",
- "srvAllowedHostsSuffix-without_dot_pass.json")
- .contains(filename));
}
public abstract MongoClient createMongoClient(MongoClientSettings settings);
@@ -284,6 +268,9 @@ private void assertOptions(final ConnectionString connectionString) {
case "srvServiceName":
assertEquals(entry.getValue().asString().getValue(), connectionString.getSrvServiceName());
break;
+ case "srvAllowedHostsSuffix":
+ assertEquals(entry.getValue().asString().getValue(), connectionString.getSrvAllowedHostsSuffix());
+ break;
default:
throw new UnsupportedOperationException("No support configured yet for " + entry.getKey());
}