> optionsMap) {
case "sockettimeoutms":
socketTimeout = parseInteger(value, "sockettimeoutms");
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;
@@ -1151,6 +1184,41 @@ private void validatePort(final String host, final String port) {
}
}
+ private void validateProxyParameters() {
+ if (proxyHost == null) {
+ if (proxyPort != null) {
+ throw new IllegalArgumentException("proxyPort can only be specified with proxyHost");
+ } else if (proxyUsername != null) {
+ throw new IllegalArgumentException("proxyUsername can only be specified with proxyHost");
+ } else if (proxyPassword != null) {
+ throw new IllegalArgumentException("proxyPassword can only be specified with proxyHost");
+ }
+ }
+ if (proxyPort != null && (proxyPort < 0 || proxyPort > 65535)) {
+ throw new IllegalArgumentException("proxyPort should be within the valid range (0 to 65535)");
+ }
+ if (proxyUsername != null) {
+ if (proxyUsername.isEmpty()) {
+ throw new IllegalArgumentException("proxyUsername cannot be empty");
+ }
+ if (proxyUsername.getBytes(StandardCharsets.UTF_8).length >= 255) {
+ throw new IllegalArgumentException("username's length in bytes cannot be greater than 255");
+ }
+ }
+ if (proxyPassword != null) {
+ if (proxyPassword.isEmpty()) {
+ throw new IllegalArgumentException("proxyPassword cannot be empty");
+ }
+ if (proxyPassword.getBytes(StandardCharsets.UTF_8).length >= 255) {
+ throw new IllegalArgumentException("password's length in bytes cannot be greater than 255");
+ }
+ }
+ if (proxyUsername == null ^ proxyPassword == null) {
+ throw new IllegalArgumentException(
+ "Both proxyUsername and proxyPassword must be set together. They cannot be set individually");
+ }
+ }
+
private int countOccurrences(final String haystack, final String needle) {
return haystack.length() - haystack.replace(needle, "").length();
}
@@ -1439,6 +1507,49 @@ public Boolean getSslEnabled() {
return sslEnabled;
}
+ /**
+ * Gets the SOCKS5 proxy host specified in the connection string.
+ *
+ * @return the proxy host value.
+ * @since 4.11
+ */
+ @Nullable
+ public String getProxyHost() {
+ return proxyHost;
+ }
+
+ /**
+ * Gets the SOCKS5 proxy port specified in the connection string.
+ *
+ * @return the proxy port value.
+ * @since 4.11
+ */
+ @Nullable
+ public Integer getProxyPort() {
+ return proxyPort;
+ }
+
+ /**
+ * Gets the SOCKS5 proxy username specified in the connection string.
+ *
+ * @return the proxy username value.
+ * @since 4.11
+ */
+ @Nullable
+ public String getProxyUsername() {
+ return proxyUsername;
+ }
+
+ /**
+ * Gets the SOCKS5 proxy password specified in the connection string.
+ *
+ * @return the proxy password value.
+ * @since 4.11
+ */
+ @Nullable
+ public String getProxyPassword() {
+ return proxyPassword;
+ }
/**
* Gets the SSL invalidHostnameAllowed value specified in the connection string.
*
@@ -1560,6 +1671,10 @@ public boolean equals(final Object o) {
&& Objects.equals(maxConnecting, that.maxConnecting)
&& Objects.equals(connectTimeout, that.connectTimeout)
&& Objects.equals(socketTimeout, that.socketTimeout)
+ && Objects.equals(proxyHost, that.proxyHost)
+ && Objects.equals(proxyPort, that.proxyPort)
+ && Objects.equals(proxyUsername, that.proxyUsername)
+ && Objects.equals(proxyPassword, that.proxyPassword)
&& Objects.equals(sslEnabled, that.sslEnabled)
&& Objects.equals(sslInvalidHostnameAllowed, that.sslInvalidHostnameAllowed)
&& Objects.equals(requiredReplicaSetName, that.requiredReplicaSetName)
@@ -1579,6 +1694,7 @@ public int hashCode() {
writeConcern, retryWrites, retryReads, readConcern, minConnectionPoolSize, maxConnectionPoolSize, maxWaitTime,
maxConnectionIdleTime, maxConnectionLifeTime, maxConnecting, connectTimeout, socketTimeout, sslEnabled,
sslInvalidHostnameAllowed, requiredReplicaSetName, serverSelectionTimeout, localThreshold, heartbeatFrequency,
- applicationName, compressorList, uuidRepresentation, srvServiceName, srvMaxHosts);
+ applicationName, compressorList, uuidRepresentation, srvServiceName, srvMaxHosts, proxyHost, proxyPort,
+ proxyUsername, proxyPassword);
}
}
diff --git a/driver-core/src/main/com/mongodb/MongoClientSettings.java b/driver-core/src/main/com/mongodb/MongoClientSettings.java
index f72cb502493..95bd04296b6 100644
--- a/driver-core/src/main/com/mongodb/MongoClientSettings.java
+++ b/driver-core/src/main/com/mongodb/MongoClientSettings.java
@@ -1067,6 +1067,7 @@ private MongoClientSettings(final Builder builder) {
.connectTimeout(builder.heartbeatConnectTimeoutMS == 0
? socketSettings.getConnectTimeout(MILLISECONDS) : builder.heartbeatConnectTimeoutMS,
MILLISECONDS)
+ .applyToProxySettings(proxyBuilder -> proxyBuilder.applySettings(socketSettings.getProxySettings()))
.build();
heartbeatSocketTimeoutSetExplicitly = builder.heartbeatSocketTimeoutMS != 0;
heartbeatConnectTimeoutSetExplicitly = builder.heartbeatConnectTimeoutMS != 0;
diff --git a/driver-core/src/main/com/mongodb/connection/ProxySettings.java b/driver-core/src/main/com/mongodb/connection/ProxySettings.java
new file mode 100644
index 00000000000..1a4c793f875
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/connection/ProxySettings.java
@@ -0,0 +1,348 @@
+/*
+ * 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.connection;
+
+
+import com.mongodb.AutoEncryptionSettings;
+import com.mongodb.ClientEncryptionSettings;
+import com.mongodb.ConnectionString;
+import com.mongodb.annotations.Immutable;
+import com.mongodb.lang.Nullable;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+import static com.mongodb.assertions.Assertions.isTrue;
+import static com.mongodb.assertions.Assertions.isTrueArgument;
+import static com.mongodb.assertions.Assertions.notNull;
+
+/**
+ * This setting is only applicable when communicating with a MongoDB server using the synchronous variant of {@code MongoClient}.
+ *
+ * This setting is furthermore ignored if:
+ *
+ * - the communication is via {@linkplain com.mongodb.UnixServerAddress Unix domain socket}.
+ * - a {@link StreamFactoryFactory} is {@linkplain com.mongodb.MongoClientSettings.Builder#streamFactoryFactory(StreamFactoryFactory)
+ * configured}.
+ *
+ *
+ * @see SocketSettings#getProxySettings()
+ * @see ClientEncryptionSettings#getKeyVaultMongoClientSettings()
+ * @see AutoEncryptionSettings#getKeyVaultMongoClientSettings()
+ * @since 4.11
+ */
+@Immutable
+public final class ProxySettings {
+
+ private static final int DEFAULT_PORT = 1080;
+ @Nullable
+ private final String host;
+
+ @Nullable
+ private final Integer port;
+
+ @Nullable
+ private final String username;
+ @Nullable
+ private final String password;
+
+ /**
+ * Creates a {@link Builder} for creating a new {@link ProxySettings} instance.
+ *
+ * @return a new {@link Builder} for {@link ProxySettings}.
+ */
+ public static ProxySettings.Builder builder() {
+ return new ProxySettings.Builder();
+ }
+
+ /**
+ * Creates a {@link Builder} for creating a new {@link ProxySettings} instance.
+ *
+ * @param proxySettings existing {@link ProxySettings} to default the builder settings on.
+ * @return a new {@link Builder} for {@link ProxySettings}.
+ */
+ public static ProxySettings.Builder builder(final ProxySettings proxySettings) {
+ return builder().applySettings(proxySettings);
+ }
+
+ /**
+ * A builder for an instance of {@code ProxySettings}.
+ */
+ public static final class Builder {
+ private String host;
+ private Integer port;
+ private String username;
+ private String password;
+
+ private Builder() {
+ }
+
+ /**
+ * Applies the provided {@link ProxySettings} to this builder instance.
+ *
+ *
+ * Note: This method overwrites all existing proxy settings previously configured in this builder.
+ *
+ * @param proxySettings The {@link ProxySettings} instance containing the proxy configuration to apply.
+ * @return This {@link ProxySettings.Builder} instance with the updated proxy settings applied.
+ * @throws IllegalArgumentException If the provided {@link ProxySettings} instance is null.
+ */
+ public ProxySettings.Builder applySettings(final ProxySettings proxySettings) {
+ notNull("ProxySettings", proxySettings);
+ this.host = proxySettings.host;
+ this.port = proxySettings.port;
+ this.username = proxySettings.username;
+ this.password = proxySettings.password;
+ return this;
+ }
+
+ /**
+ * Sets the SOCKS5 proxy host to establish a connection through.
+ *
+ *
The host can be specified as an IPv4 address (e.g., "192.168.1.1"),
+ * an IPv6 address (e.g., "2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
+ * or a domain name (e.g., "proxy.example.com").
+ *
+ * @param host The SOCKS5 proxy host to set.
+ * @return This ProxySettings.Builder instance, configured with the specified proxy host.
+ * @throws IllegalArgumentException If the provided host is null or empty after trimming.
+ * @see ProxySettings.Builder#port(int)
+ * @see #getHost()
+ */
+ public ProxySettings.Builder host(final String host) {
+ notNull("proxyHost", host);
+ isTrueArgument("proxyHost is not empty", host.trim().length() > 0);
+ this.host = host;
+ return this;
+ }
+
+ /**
+ * Sets the port number for the SOCKS5 proxy server. The port should be a non-negative integer
+ * representing the port through which the SOCKS5 proxy connection will be established.
+ *
+ * If a port is specified via this method, a corresponding host must be provided using the {@link #host(String)} method.
+ *
+ * If no port is provided, the default port 1080 will be used.
+ *
+ * @param port The port number to set for the SOCKS5 proxy server.
+ * @return This ProxySettings.Builder instance, configured with the specified proxy port.
+ * @throws IllegalArgumentException If the provided port is negative.
+ * @see ProxySettings.Builder#host(String)
+ * @see #getPort()
+ */
+ public ProxySettings.Builder port(final int port) {
+ isTrueArgument("proxyPort is within the valid range (0 to 65535)", port >= 0 && port <= 65535);
+ this.port = port;
+ return this;
+ }
+
+ /**
+ * Sets the username for authenticating with the SOCKS5 proxy server.
+ * The provided username should not be empty or null.
+ *
+ * If a username is specified, the corresponding password and proxy host must also be specified using the
+ * {@link #password(String)} and {@link #host(String)} methods, respectively.
+ *
+ * @param username The username to set for proxy authentication.
+ * @return This ProxySettings.Builder instance, configured with the specified username.
+ * @throws IllegalArgumentException If the provided username is empty or null.
+ * @see ProxySettings.Builder#password(String)
+ * @see ProxySettings.Builder#host(String)
+ * @see #getUsername()
+ */
+ public ProxySettings.Builder username(final String username) {
+ notNull("username", username);
+ isTrueArgument("username is not empty", !username.isEmpty());
+ isTrueArgument("username's length in bytes is not greater than 255",
+ username.getBytes(StandardCharsets.UTF_8).length <= 255);
+ this.username = username;
+ return this;
+ }
+
+ /**
+ * Sets the password for authenticating with the SOCKS5 proxy server.
+ * The provided password should not be empty or null.
+ *
+ * If a password is specified, the corresponding username and proxy host must also be specified using the
+ * {@link #username(String)} and {@link #host(String)} methods, respectively.
+ *
+ * @param password The password to set for proxy authentication.
+ * @return This ProxySettings.Builder instance, configured with the specified password.
+ * @throws IllegalArgumentException If the provided password is empty or null.
+ * @see ProxySettings.Builder#username(String)
+ * @see ProxySettings.Builder#host(String)
+ * @see #getPassword()
+ */
+ public ProxySettings.Builder password(final String password) {
+ notNull("password", password);
+ isTrueArgument("password is not empty", !password.isEmpty());
+ isTrueArgument("password's length in bytes is not greater than 255",
+ password.getBytes(StandardCharsets.UTF_8).length <= 255);
+ this.password = password;
+ return this;
+ }
+
+
+ /**
+ * Takes the proxy settings from the given {@code ConnectionString} and applies them to the {@link Builder}.
+ *
+ * @param connectionString the connection string containing details of how to connect to proxy server.
+ * @return this.
+ * @see ConnectionString#getProxyHost()
+ * @see ConnectionString#getProxyPort()
+ * @see ConnectionString#getProxyUsername()
+ * @see ConnectionString#getProxyPassword()
+ */
+ public ProxySettings.Builder applyConnectionString(final ConnectionString connectionString) {
+ String proxyHost = connectionString.getProxyHost();
+ if (proxyHost != null) {
+ this.host(proxyHost);
+ }
+
+ Integer proxyPort = connectionString.getProxyPort();
+ if (proxyPort != null) {
+ this.port(proxyPort);
+ }
+
+ String proxyUsername = connectionString.getProxyUsername();
+ if (proxyUsername != null) {
+ this.username(proxyUsername);
+ }
+
+ String proxyPassword = connectionString.getProxyPassword();
+ if (proxyPassword != null) {
+ this.password(proxyPassword);
+ }
+
+ return this;
+ }
+
+ /**
+ * Build an instance of {@code ProxySettings}.
+ *
+ * @return the {@link ProxySettings}.
+ */
+ public ProxySettings build() {
+ return new ProxySettings(this);
+ }
+ }
+
+ /**
+ * Gets the SOCKS5 proxy host.
+ *
+ * @return the proxy host value. {@code null} if and only if the {@linkplain #isProxyEnabled() proxy functionality is not enabled}.
+ * @see Builder#host(String)
+ */
+ @Nullable
+ public String getHost() {
+ return host;
+ }
+
+ /**
+ * Gets the SOCKS5 proxy port.
+ *
+ * @return The port number of the SOCKS5 proxy. If a custom port has been set using {@link Builder#port(int)},
+ * that custom port value is returned. Otherwise, the default SOCKS5 port {@value #DEFAULT_PORT} is returned.
+ * @see Builder#port(int)
+ */
+ public int getPort() {
+ if (port != null) {
+ return port;
+ }
+ return DEFAULT_PORT;
+ }
+
+ /**
+ * Gets the SOCKS5 proxy username.
+ *
+ * @return the proxy username value.
+ * @see Builder#username(String)
+ */
+ @Nullable
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Gets the SOCKS5 proxy password.
+ *
+ * @return the proxy password value.
+ * @see Builder#password(String)
+ */
+ @Nullable
+ public String getPassword() {
+ return password;
+ }
+
+ /**
+ * Checks if the SOCKS5 proxy is enabled.
+ *
+ * @return {@code true} if the proxy is enabled, {@code false} otherwise.
+ * @see Builder#host(String)
+ */
+ public boolean isProxyEnabled() {
+ return host != null;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final ProxySettings that = (ProxySettings) o;
+ return Objects.equals(host, that.host)
+ && Objects.equals(port, that.port)
+ && Objects.equals(username, that.username)
+ && Objects.equals(password, that.password);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(host, port, username, password);
+ }
+
+ @Override
+ public String toString() {
+ return "ProxySettings{"
+ + "host=" + host
+ + ", port=" + port
+ + ", username=" + username
+ + ", password=" + password
+ + '}';
+ }
+
+ private ProxySettings(final ProxySettings.Builder builder) {
+ if (builder.host == null) {
+ isTrue("proxyPort can only be specified with proxyHost",
+ builder.port == null);
+ isTrue("proxyPassword can only be specified with proxyHost",
+ builder.password == null);
+ isTrue("proxyUsername can only be specified with proxyHost",
+ builder.username == null);
+ }
+ isTrue("Both proxyUsername and proxyPassword must be set together. They cannot be set individually",
+ (builder.username == null) == (builder.password == null));
+
+ this.host = builder.host;
+ this.port = builder.port;
+ this.username = builder.username;
+ this.password = builder.password;
+ }
+}
+
diff --git a/driver-core/src/main/com/mongodb/connection/SocketSettings.java b/driver-core/src/main/com/mongodb/connection/SocketSettings.java
index 62e21725847..7a63790cb66 100644
--- a/driver-core/src/main/com/mongodb/connection/SocketSettings.java
+++ b/driver-core/src/main/com/mongodb/connection/SocketSettings.java
@@ -16,6 +16,7 @@
package com.mongodb.connection;
+import com.mongodb.Block;
import com.mongodb.ConnectionString;
import com.mongodb.annotations.Immutable;
@@ -35,9 +36,11 @@ public final class SocketSettings {
private final long readTimeoutMS;
private final int receiveBufferSize;
private final int sendBufferSize;
+ private final ProxySettings proxySettings;
/**
* Gets a builder for an instance of {@code SocketSettings}.
+ *
* @return the builder
*/
public static Builder builder() {
@@ -63,6 +66,7 @@ public static final class Builder {
private long readTimeoutMS;
private int receiveBufferSize;
private int sendBufferSize;
+ private ProxySettings.Builder proxySettingsBuilder = ProxySettings.builder();
private Builder() {
}
@@ -82,6 +86,7 @@ public Builder applySettings(final SocketSettings socketSettings) {
readTimeoutMS = socketSettings.readTimeoutMS;
receiveBufferSize = socketSettings.receiveBufferSize;
sendBufferSize = socketSettings.sendBufferSize;
+ proxySettingsBuilder.applySettings(socketSettings.getProxySettings());
return this;
}
@@ -132,6 +137,18 @@ public Builder sendBufferSize(final int sendBufferSize) {
return this;
}
+ /**
+ * Applies the {@link ProxySettings.Builder} block and then sets the {@link SocketSettings#proxySettings}.
+ *
+ * @param block the block to apply to the {@link ProxySettings}.
+ * @return this
+ * @see SocketSettings#getProxySettings()
+ */
+ public SocketSettings.Builder applyToProxySettings(final Block block) {
+ notNull("block", block).apply(proxySettingsBuilder);
+ return this;
+ }
+
/**
* Takes the settings from the given {@code ConnectionString} and applies them to the builder
*
@@ -151,6 +168,8 @@ public Builder applyConnectionString(final ConnectionString connectionString) {
this.readTimeout(socketTimeout, MILLISECONDS);
}
+ proxySettingsBuilder.applyConnectionString(connectionString);
+
return this;
}
@@ -184,8 +203,20 @@ public int getReadTimeout(final TimeUnit timeUnit) {
return (int) timeUnit.convert(readTimeoutMS, MILLISECONDS);
}
+ /**
+ * Gets the proxy settings used for connecting to MongoDB via a SOCKS5 proxy server.
+ *
+ * @return The {@link ProxySettings} instance containing the SOCKS5 proxy configuration.
+ * @see Builder#applyToProxySettings(Block)
+ * @since 4.11
+ */
+ public ProxySettings getProxySettings() {
+ return proxySettings;
+ }
+
/**
* Gets the receive buffer size. Defaults to the operating system default.
+ *
* @return the receive buffer size
*/
public int getReceiveBufferSize() {
@@ -224,8 +255,7 @@ public boolean equals(final Object o) {
if (sendBufferSize != that.sendBufferSize) {
return false;
}
-
- return true;
+ return proxySettings.equals(that.proxySettings);
}
@Override
@@ -234,17 +264,18 @@ public int hashCode() {
result = 31 * result + (int) (readTimeoutMS ^ (readTimeoutMS >>> 32));
result = 31 * result + receiveBufferSize;
result = 31 * result + sendBufferSize;
+ result = 31 * result + proxySettings.hashCode();
return result;
}
@Override
public String toString() {
return "SocketSettings{"
- + "connectTimeoutMS=" + connectTimeoutMS
- + ", readTimeoutMS=" + readTimeoutMS
- + ", receiveBufferSize=" + receiveBufferSize
- + ", sendBufferSize=" + sendBufferSize
- + '}';
+ + "connectTimeoutMS=" + connectTimeoutMS
+ + ", readTimeoutMS=" + readTimeoutMS
+ + ", receiveBufferSize=" + receiveBufferSize
+ + ", proxySettings=" + proxySettings
+ + '}';
}
private SocketSettings(final Builder builder) {
@@ -252,5 +283,6 @@ private SocketSettings(final Builder builder) {
readTimeoutMS = builder.readTimeoutMS;
receiveBufferSize = builder.receiveBufferSize;
sendBufferSize = builder.sendBufferSize;
+ proxySettings = builder.proxySettingsBuilder.build();
}
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java b/driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java
new file mode 100644
index 00000000000..a1f0938e104
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java
@@ -0,0 +1,30 @@
+/*
+ * 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.internal.connection;
+
+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,6}|localhost))$");
+
+ static boolean isDomainName(final String domainName) {
+ return DOMAIN_PATTERN.matcher(domainName).matches();
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/InetAddressUtils.java b/driver-core/src/main/com/mongodb/internal/connection/InetAddressUtils.java
new file mode 100644
index 00000000000..9d82947671a
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/connection/InetAddressUtils.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2008-present MongoDB, Inc.
+ * Copyright (C) 2008 The Guava Authors
+ *
+ * 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.internal.connection;
+
+import com.mongodb.lang.Nullable;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+
+/**
+ * Static utility methods pertaining to {@link InetAddress} instances.
+ *
+ * Important note: Unlike {@link java.net.InetAddress#getByName(String)}, the methods of this class never
+ * cause DNS services to be accessed. For this reason, you should prefer these methods as much as
+ * possible over their JDK equivalents whenever you are expecting to handle only IP address string
+ * literals -- there is no blocking DNS penalty for a malformed string.
+ */
+final class InetAddressUtils {
+ private static final int IPV4_PART_COUNT = 4;
+ private static final int IPV6_PART_COUNT = 8;
+ private static final char IPV4_DELIMITER = '.';
+ private static final char IPV6_DELIMITER = ':';
+
+ private InetAddressUtils() {
+ }
+
+ /**
+ * Returns the {@link InetAddress} having the given string representation.
+ *
+ *
This deliberately avoids all nameservice lookups (e.g. no DNS).
+ *
+ *
Anything after a {@code %} in an IPv6 address is ignored (assumed to be a Scope ID).
+ *
+ *
This method accepts non-ASCII digits, for example {@code "192.168.0.1"} (those are fullwidth
+ * characters). That is consistent with {@link InetAddress}, but not with various RFCs.
+ *
+ * @param ipString {@code String} containing an IPv4 or IPv6 string literal, e.g. {@code
+ * "192.168.0.1"} or {@code "2001:db8::1"}
+ * @return {@link InetAddress} representing the argument
+ * @throws IllegalArgumentException if the argument is not a valid IP string literal
+ */
+ static InetAddress forString(final String ipString) {
+ byte[] addr = ipStringToBytes(ipString);
+
+ // The argument was malformed, i.e. not an IP string literal.
+ if (addr == null) {
+ throw new IllegalArgumentException(ipString + " IP address is incorrect");
+ }
+
+ return bytesToInetAddress(addr);
+ }
+
+ /**
+ * Returns {@code true} if the supplied string is a valid IP string literal, {@code false}
+ * otherwise.
+ *
+ *
This method accepts non-ASCII digits, for example {@code "192.168.0.1"} (those are fullwidth
+ * characters). That is consistent with {@link InetAddress}, but not with various RFCs.
+ *
+ * @param ipString {@code String} to evaluated as an IP string literal
+ * @return {@code true} if the argument is a valid IP string literal
+ */
+ static boolean isInetAddress(final String ipString) {
+ return ipStringToBytes(ipString) != null;
+ }
+
+ /**
+ * Returns {@code null} if unable to parse into a {@code byte[]}.
+ */
+ @Nullable
+ static byte[] ipStringToBytes(final String ipStringParam) {
+ String ipString = ipStringParam;
+ // Make a first pass to categorize the characters in this string.
+ boolean hasColon = false;
+ boolean hasDot = false;
+ int percentIndex = -1;
+ for (int i = 0; i < ipString.length(); i++) {
+ char c = ipString.charAt(i);
+ if (c == '.') {
+ hasDot = true;
+ } else if (c == ':') {
+ if (hasDot) {
+ return null; // Colons must not appear after dots.
+ }
+ hasColon = true;
+ } else if (c == '%') {
+ percentIndex = i;
+ break; // everything after a '%' is ignored (it's a Scope ID): http://superuser.com/a/99753
+ } else if (Character.digit(c, 16) == -1) {
+ return null; // Everything else must be a decimal or hex digit.
+ }
+ }
+
+ // Now decide which address family to parse.
+ if (hasColon) {
+ if (hasDot) {
+ ipString = convertDottedQuadToHex(ipString);
+ if (ipString == null) {
+ return null;
+ }
+ }
+ if (percentIndex != -1) {
+ ipString = ipString.substring(0, percentIndex);
+ }
+ return textToNumericFormatV6(ipString);
+ } else if (hasDot) {
+ if (percentIndex != -1) {
+ return null; // Scope IDs are not supported for IPV4
+ }
+ return textToNumericFormatV4(ipString);
+ }
+ return null;
+ }
+
+ private static boolean hasCorrectNumberOfOctets(final String sequence) {
+ int matches = 3;
+ int index = 0;
+ while (matches-- > 0) {
+ index = sequence.indexOf(IPV4_DELIMITER, index);
+ if (index == -1) {
+ return false;
+ }
+ index++;
+ }
+ return sequence.indexOf(IPV4_DELIMITER, index) == -1;
+ }
+
+ private static int countIn(final CharSequence sequence, final char character) {
+ int count = 0;
+ for (int i = 0; i < sequence.length(); i++) {
+ if (sequence.charAt(i) == character) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ @Nullable
+ private static byte[] textToNumericFormatV4(final String ipString) {
+ if (!hasCorrectNumberOfOctets(ipString)) {
+ return null; // Wrong number of parts
+ }
+
+ byte[] bytes = new byte[IPV4_PART_COUNT];
+ int start = 0;
+ // Iterate through the parts of the ip string.
+ // Invariant: start is always the beginning of an octet.
+ for (int i = 0; i < IPV4_PART_COUNT; i++) {
+ int end = ipString.indexOf(IPV4_DELIMITER, start);
+ if (end == -1) {
+ end = ipString.length();
+ }
+ try {
+ bytes[i] = parseOctet(ipString, start, end);
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ start = end + 1;
+ }
+
+ return bytes;
+ }
+
+ @Nullable
+ private static byte[] textToNumericFormatV6(final String ipString) {
+ // An address can have [2..8] colons.
+ int delimiterCount = countIn(ipString, IPV6_DELIMITER);
+ if (delimiterCount < 2 || delimiterCount > IPV6_PART_COUNT) {
+ return null;
+ }
+ int partsSkipped = IPV6_PART_COUNT - (delimiterCount + 1); // estimate; may be modified later
+ boolean hasSkip = false;
+ // Scan for the appearance of ::, to mark a skip-format IPV6 string and adjust the partsSkipped
+ // estimate.
+ for (int i = 0; i < ipString.length() - 1; i++) {
+ if (ipString.charAt(i) == IPV6_DELIMITER && ipString.charAt(i + 1) == IPV6_DELIMITER) {
+ if (hasSkip) {
+ return null; // Can't have more than one ::
+ }
+ hasSkip = true;
+ partsSkipped++; // :: means we skipped an extra part in between the two delimiters.
+ if (i == 0) {
+ partsSkipped++; // Begins with ::, so we skipped the part preceding the first :
+ }
+ if (i == ipString.length() - 2) {
+ partsSkipped++; // Ends with ::, so we skipped the part after the last :
+ }
+ }
+ }
+ if (ipString.charAt(0) == IPV6_DELIMITER && ipString.charAt(1) != IPV6_DELIMITER) {
+ return null; // ^: requires ^::
+ }
+ if (ipString.charAt(ipString.length() - 1) == IPV6_DELIMITER
+ && ipString.charAt(ipString.length() - 2) != IPV6_DELIMITER) {
+ return null; // :$ requires ::$
+ }
+ if (hasSkip && partsSkipped <= 0) {
+ return null; // :: must expand to at least one '0'
+ }
+ if (!hasSkip && delimiterCount + 1 != IPV6_PART_COUNT) {
+ return null; // Incorrect number of parts
+ }
+
+ ByteBuffer rawBytes = ByteBuffer.allocate(2 * IPV6_PART_COUNT);
+ try {
+ // Iterate through the parts of the ip string.
+ // Invariant: start is always the beginning of a hextet, or the second ':' of the skip
+ // sequence "::"
+ int start = 0;
+ if (ipString.charAt(0) == IPV6_DELIMITER) {
+ start = 1;
+ }
+ while (start < ipString.length()) {
+ int end = ipString.indexOf(IPV6_DELIMITER, start);
+ if (end == -1) {
+ end = ipString.length();
+ }
+ if (ipString.charAt(start) == IPV6_DELIMITER) {
+ // expand zeroes
+ for (int i = 0; i < partsSkipped; i++) {
+ rawBytes.putShort((short) 0);
+ }
+
+ } else {
+ rawBytes.putShort(parseHextet(ipString, start, end));
+ }
+ start = end + 1;
+ }
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ return rawBytes.array();
+ }
+
+ @Nullable
+ private static String convertDottedQuadToHex(final String ipString) {
+ int lastColon = ipString.lastIndexOf(':');
+ String initialPart = ipString.substring(0, lastColon + 1);
+ String dottedQuad = ipString.substring(lastColon + 1);
+ byte[] quad = textToNumericFormatV4(dottedQuad);
+ if (quad == null) {
+ return null;
+ }
+ String penultimate = Integer.toHexString(((quad[0] & 0xff) << 8) | (quad[1] & 0xff));
+ String ultimate = Integer.toHexString(((quad[2] & 0xff) << 8) | (quad[3] & 0xff));
+ return initialPart + penultimate + ":" + ultimate;
+ }
+
+ private static byte parseOctet(final String ipString, final int start, final int end) {
+ // Note: we already verified that this string contains only hex digits, but the string may still
+ // contain non-decimal characters.
+ int length = end - start;
+ if (length <= 0 || length > 3) {
+ throw new NumberFormatException();
+ }
+ // Disallow leading zeroes, because no clear standard exists on
+ // whether these should be interpreted as decimal or octal.
+ if (length > 1 && ipString.charAt(start) == '0') {
+ throw new NumberFormatException("IP address octal representation is not supported");
+ }
+ int octet = 0;
+ for (int i = start; i < end; i++) {
+ octet *= 10;
+ int digit = Character.digit(ipString.charAt(i), 10);
+ if (digit < 0) {
+ throw new NumberFormatException();
+ }
+ octet += digit;
+ }
+ if (octet > 255) {
+ throw new NumberFormatException();
+ }
+ return (byte) octet;
+ }
+
+ // Parse a hextet out of the ipString from start (inclusive) to end (exclusive)
+ private static short parseHextet(final String ipString, final int start, final int end) {
+ // Note: we already verified that this string contains only hex digits.
+ int length = end - start;
+ if (length <= 0 || length > 4) {
+ throw new NumberFormatException();
+ }
+ int hextet = 0;
+ for (int i = start; i < end; i++) {
+ hextet = hextet << 4;
+ hextet |= Character.digit(ipString.charAt(i), 16);
+ }
+ return (short) hextet;
+ }
+
+ /**
+ * Convert a byte array into an InetAddress.
+ *
+ *
{@link InetAddress#getByAddress} is documented as throwing a checked exception "if IP
+ * address is of illegal length." We replace it with an unchecked exception, for use by callers
+ * who already know that addr is an array of length 4 or 16.
+ *
+ * @param addr the raw 4-byte or 16-byte IP address in big-endian order
+ * @return an InetAddress object created from the raw IP address
+ */
+ private static InetAddress bytesToInetAddress(final byte[] addr) {
+ try {
+ return InetAddress.getByAddress(addr);
+ } catch (UnknownHostException e) {
+ throw new AssertionError(e);
+ }
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocketStream.java b/driver-core/src/main/com/mongodb/internal/connection/SocketStream.java
index 7360ce5f57b..dd8418bf5a5 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/SocketStream.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/SocketStream.java
@@ -22,12 +22,15 @@
import com.mongodb.ServerAddress;
import com.mongodb.connection.AsyncCompletionHandler;
import com.mongodb.connection.BufferProvider;
+import com.mongodb.connection.ProxySettings;
import com.mongodb.connection.SocketSettings;
import com.mongodb.connection.SslSettings;
import com.mongodb.connection.Stream;
import org.bson.ByteBuf;
import javax.net.SocketFactory;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -36,8 +39,13 @@
import java.net.SocketTimeoutException;
import java.util.Iterator;
import java.util.List;
+import java.util.concurrent.TimeUnit;
+import static com.mongodb.assertions.Assertions.assertTrue;
import static com.mongodb.assertions.Assertions.notNull;
+import static com.mongodb.internal.connection.SocketStreamHelper.configureSocket;
+import static com.mongodb.internal.connection.SslHelper.configureSslSocket;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
*
This class is not part of the public API and may be removed or changed at any time
@@ -75,6 +83,16 @@ public void open() {
}
protected Socket initializeSocket() throws IOException {
+ ProxySettings proxySettings = settings.getProxySettings();
+ if (proxySettings.isProxyEnabled()) {
+ if (sslSettings.isEnabled()) {
+ assertTrue(socketFactory instanceof SSLSocketFactory);
+ SSLSocketFactory sslSocketFactory = (SSLSocketFactory) socketFactory;
+ return initializeSslSocketOverSocksProxy(sslSocketFactory);
+ }
+ return initializeSocketOverSocksProxy();
+ }
+
Iterator inetSocketAddresses = address.getSocketAddresses().iterator();
while (inetSocketAddresses.hasNext()) {
Socket socket = socketFactory.createSocket();
@@ -91,6 +109,46 @@ protected Socket initializeSocket() throws IOException {
throw new MongoSocketException("Exception opening socket", getAddress());
}
+ private SSLSocket initializeSslSocketOverSocksProxy(final SSLSocketFactory sslSocketFactory) throws IOException {
+ final String serverHost = address.getHost();
+ final int serverPort = address.getPort();
+
+ SocksSocket socksProxy = new SocksSocket(settings.getProxySettings());
+ configureSocket(socksProxy, settings);
+ InetSocketAddress inetSocketAddress = toSocketAddress(serverHost, serverPort);
+ socksProxy.connect(inetSocketAddress, settings.getConnectTimeout(MILLISECONDS));
+
+ SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socksProxy, serverHost, serverPort, true);
+ //Even though Socks proxy connection is already established, TLS handshake has not been performed yet.
+ //So it is possible to set SSL parameters before handshake is done.
+ configureSslSocket(sslSocket, sslSettings, inetSocketAddress);
+ return sslSocket;
+ }
+
+
+ /**
+ * Creates an unresolved {@link InetSocketAddress}.
+ * This method is used to create an address that is meant to be resolved by a SOCKS proxy.
+ */
+ private static InetSocketAddress toSocketAddress(final String serverHost, final int serverPort) {
+ return InetSocketAddress.createUnresolved(serverHost, serverPort);
+ }
+
+ private Socket initializeSocketOverSocksProxy() throws IOException {
+ Socket createdSocket = socketFactory.createSocket();
+ configureSocket(createdSocket, settings);
+ /*
+ Wrap the configured socket with SocksSocket to add extra functionality.
+ Reason for separate steps: We can't directly extend Java 11 methods within 'SocksSocket'
+ to configure itself.
+ */
+ SocksSocket socksProxy = new SocksSocket(createdSocket, settings.getProxySettings());
+
+ socksProxy.connect(toSocketAddress(address.getHost(), address.getPort()),
+ settings.getConnectTimeout(TimeUnit.MILLISECONDS));
+ return socksProxy;
+ }
+
@Override
public ByteBuf getBuffer(final int size) {
return bufferProvider.getBuffer(size);
diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocketStreamHelper.java b/driver-core/src/main/com/mongodb/internal/connection/SocketStreamHelper.java
index 5d7a2b705ab..1b5e789e646 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/SocketStreamHelper.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/SocketStreamHelper.java
@@ -16,21 +16,18 @@
package com.mongodb.internal.connection;
-import com.mongodb.MongoInternalException;
import com.mongodb.connection.SocketSettings;
import com.mongodb.connection.SslSettings;
-import javax.net.ssl.SSLParameters;
-import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.Socket;
+import java.net.SocketException;
import java.net.SocketOption;
-import static com.mongodb.internal.connection.SslHelper.enableHostNameVerification;
-import static com.mongodb.internal.connection.SslHelper.enableSni;
+import static com.mongodb.internal.connection.SslHelper.configureSslSocket;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@SuppressWarnings({"unchecked", "rawtypes"})
@@ -74,6 +71,12 @@ final class SocketStreamHelper {
static void initialize(final Socket socket, final InetSocketAddress inetSocketAddress, final SocketSettings settings,
final SslSettings sslSettings) throws IOException {
+ configureSocket(socket, settings);
+ configureSslSocket(socket, sslSettings, inetSocketAddress);
+ socket.connect(inetSocketAddress, settings.getConnectTimeout(MILLISECONDS));
+ }
+
+ static void configureSocket(final Socket socket, final SocketSettings settings) throws SocketException {
socket.setTcpNoDelay(true);
socket.setSoTimeout(settings.getReadTimeout(MILLISECONDS));
socket.setKeepAlive(true);
@@ -87,24 +90,6 @@ static void initialize(final Socket socket, final InetSocketAddress inetSocketAd
if (settings.getSendBufferSize() > 0) {
socket.setSendBufferSize(settings.getSendBufferSize());
}
- if (sslSettings.isEnabled() || socket instanceof SSLSocket) {
- if (!(socket instanceof SSLSocket)) {
- throw new MongoInternalException("SSL is enabled but the socket is not an instance of javax.net.ssl.SSLSocket");
- }
- SSLSocket sslSocket = (SSLSocket) socket;
- SSLParameters sslParameters = sslSocket.getSSLParameters();
- if (sslParameters == null) {
- sslParameters = new SSLParameters();
- }
-
- enableSni(inetSocketAddress.getHostName(), sslParameters);
-
- if (!sslSettings.isInvalidHostNameAllowed()) {
- enableHostNameVerification(sslParameters);
- }
- sslSocket.setSSLParameters(sslParameters);
- }
- socket.connect(inetSocketAddress, settings.getConnectTimeout(MILLISECONDS));
}
static void setExtendedSocketOptions(final Socket socket) {
diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java
new file mode 100644
index 00000000000..6d19d7f5a5c
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java
@@ -0,0 +1,779 @@
+/*
+ * 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.internal.connection;
+
+import com.mongodb.connection.ProxySettings;
+import com.mongodb.internal.Timeout;
+import com.mongodb.lang.Nullable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ConnectException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+import static com.mongodb.assertions.Assertions.assertFalse;
+import static com.mongodb.assertions.Assertions.assertNotNull;
+import static com.mongodb.assertions.Assertions.assertTrue;
+import static com.mongodb.assertions.Assertions.fail;
+import static com.mongodb.assertions.Assertions.isTrueArgument;
+import static com.mongodb.internal.connection.DomainNameUtils.isDomainName;
+import static com.mongodb.internal.connection.SocksSocket.AddressType.DOMAIN_NAME;
+import static com.mongodb.internal.connection.SocksSocket.AddressType.IP_V4;
+import static com.mongodb.internal.connection.SocksSocket.AddressType.IP_V6;
+import static com.mongodb.internal.connection.SocksSocket.ServerReply.REPLY_SUCCEEDED;
+
+/**
+ * This class is not part of the public API and may be removed or changed at any time
+ */
+public final class SocksSocket extends Socket {
+ private static final byte SOCKS_VERSION = 0x05;
+ private static final byte RESERVED = 0x00;
+ private static final byte PORT_LENGTH = 2;
+ private static final byte AUTHENTICATION_SUCCEEDED_STATUS = 0x00;
+ public static final String IP_PARSING_ERROR_SUFFIX = " is not an IP string literal";
+ private static final byte USER_PASSWORD_SUB_NEGOTIATION_VERSION = 0x01;
+ private InetSocketAddress remoteAddress;
+ private final ProxySettings proxySettings;
+ @Nullable
+ private final Socket socket;
+
+ public SocksSocket(final ProxySettings proxySettings) {
+ this(null, proxySettings);
+ }
+
+ public SocksSocket(@Nullable final Socket socket, final ProxySettings proxySettings) {
+ assertNotNull(proxySettings.getHost());
+ /* Explanation for using Socket instead of SocketFactory: The process of initializing a socket for a SOCKS proxy follows a specific sequence.
+ First, a basic TCP socket is created using the socketFactory, and then it's customized with settings.
+ Subsequently, the socket is wrapped within a SocksSocket instance to provide additional functionality.
+ Due to limitations in extending methods within SocksSocket for Java 11, the configuration step must precede the wrapping stage.
+ As a result, passing SocketFactory directly into this constructor for socket creation is not feasible.
+ */
+ if (socket != null) {
+ assertFalse(socket.isConnected());
+ }
+ this.socket = socket;
+ this.proxySettings = proxySettings;
+ }
+
+ @Override
+ public void connect(final SocketAddress endpoint, final int timeoutMs) throws IOException {
+ // `Socket` requires `IllegalArgumentException`
+ isTrueArgument("timeoutMs", timeoutMs >= 0);
+ try {
+ Timeout timeout = toTimeout(timeoutMs);
+ InetSocketAddress unresolvedAddress = (InetSocketAddress) endpoint;
+ assertTrue(unresolvedAddress.isUnresolved());
+ this.remoteAddress = unresolvedAddress;
+
+ InetSocketAddress proxyAddress = new InetSocketAddress(assertNotNull(proxySettings.getHost()), proxySettings.getPort());
+ if (socket != null) {
+ socket.connect(proxyAddress, remainingMillis(timeout));
+ } else {
+ super.connect(proxyAddress, remainingMillis(timeout));
+ }
+ SocksAuthenticationMethod authenticationMethod = performNegotiation(timeout);
+ authenticate(authenticationMethod, timeout);
+ sendConnect(timeout);
+ } catch (SocketException socketException) {
+ /*
+ * The 'close()' call here has two purposes:
+ *
+ * 1. Enforces self-closing under RFC 1928 if METHOD is X'FF'.
+ * 2. Handles all other errors during connection, distinct from external closures.
+ */
+ close();
+ throw socketException;
+ }
+ }
+
+ private void sendConnect(final Timeout timeout) throws IOException {
+ final String host = remoteAddress.getHostName();
+ final int port = remoteAddress.getPort();
+ final byte[] bytesOfHost = host.getBytes(StandardCharsets.US_ASCII);
+ final int hostLength = bytesOfHost.length;
+
+ AddressType addressType;
+ byte[] ipAddress = null;
+ if (isDomainName(host)) {
+ addressType = DOMAIN_NAME;
+ } else {
+ ipAddress = createByteArrayFromIpAddress(host);
+ addressType = determineAddressType(ipAddress);
+ }
+ byte[] bufferSent = createBuffer(addressType, hostLength);
+ bufferSent[0] = SOCKS_VERSION;
+ bufferSent[1] = SocksCommand.CONNECT.getCommandNumber();
+ bufferSent[2] = RESERVED;
+ switch (addressType) {
+ case DOMAIN_NAME:
+ bufferSent[3] = DOMAIN_NAME.getAddressTypeNumber();
+ bufferSent[4] = (byte) hostLength;
+ System.arraycopy(bytesOfHost, 0, bufferSent, 5, hostLength);
+ addPort(bufferSent, 5 + hostLength, port);
+ break;
+ case IP_V4:
+ bufferSent[3] = IP_V4.getAddressTypeNumber();
+ System.arraycopy(ipAddress, 0, bufferSent, 4, ipAddress.length);
+ addPort(bufferSent, 4 + ipAddress.length, port);
+ break;
+ case IP_V6:
+ bufferSent[3] = DOMAIN_NAME.getAddressTypeNumber();
+ System.arraycopy(ipAddress, 0, bufferSent, 4, ipAddress.length);
+ addPort(bufferSent, 4 + ipAddress.length, port);
+ break;
+ default:
+ fail();
+ }
+ OutputStream outputStream = getOutputStream();
+ outputStream.write(bufferSent);
+ outputStream.flush();
+ checkServerReply(timeout);
+ }
+
+ private static void addPort(final byte[] bufferSent, final int index, final int port) {
+ bufferSent[index] = (byte) (port >> 8);
+ 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;
+ } else if (ipAddress.length == IP_V6.getLength()) {
+ return IP_V6;
+ }
+ throw fail();
+ }
+
+ private static byte[] createBuffer(final AddressType addressType, final int hostLength) {
+ switch (addressType) {
+ case DOMAIN_NAME:
+ return new byte[7 + hostLength];
+ case IP_V4:
+ return new byte[6 + IP_V4.getLength()];
+ case IP_V6:
+ return new byte[6 + IP_V6.getLength()];
+ default:
+ throw fail();
+ }
+ }
+
+ private void checkServerReply(final Timeout timeout) throws IOException {
+ byte[] data = readSocksReply(4, timeout);
+ ServerReply reply = ServerReply.of(data[1]);
+ if (reply == REPLY_SUCCEEDED) {
+ switch (AddressType.of(data[3])) {
+ case DOMAIN_NAME:
+ byte hostNameLength = readSocksReply(1, timeout)[0];
+ readSocksReply(hostNameLength + PORT_LENGTH, timeout);
+ break;
+ case IP_V4:
+ readSocksReply(IP_V4.getLength() + PORT_LENGTH, timeout);
+ break;
+ case IP_V6:
+ readSocksReply(IP_V6.getLength() + PORT_LENGTH, timeout);
+ break;
+ default:
+ throw fail();
+ }
+ return;
+ }
+ throw new ConnectException(reply.getMessage());
+ }
+
+ private void authenticate(final SocksAuthenticationMethod authenticationMethod, final Timeout timeout) throws IOException {
+ if (authenticationMethod == SocksAuthenticationMethod.USERNAME_PASSWORD) {
+ final byte[] bytesOfUsername = assertNotNull(proxySettings.getUsername()).getBytes(StandardCharsets.UTF_8);
+ final byte[] bytesOfPassword = assertNotNull(proxySettings.getPassword()).getBytes(StandardCharsets.UTF_8);
+ final int usernameLength = bytesOfUsername.length;
+ final int passwordLength = bytesOfPassword.length;
+ final byte[] command = new byte[3 + usernameLength + passwordLength];
+
+ command[0] = USER_PASSWORD_SUB_NEGOTIATION_VERSION;
+ command[1] = (byte) usernameLength;
+ System.arraycopy(bytesOfUsername, 0, command, 2, usernameLength);
+ command[2 + usernameLength] = (byte) passwordLength;
+ System.arraycopy(bytesOfPassword, 0, command, 3 + usernameLength,
+ passwordLength);
+
+ OutputStream outputStream = getOutputStream();
+ outputStream.write(command);
+ outputStream.flush();
+
+ byte[] authResult = readSocksReply(2, timeout);
+ byte authStatus = authResult[1];
+
+ if (authStatus != AUTHENTICATION_SUCCEEDED_STATUS) {
+ throw new ConnectException("Authentication failed. Proxy server returned status: " + authStatus);
+ }
+ }
+ }
+
+ private SocksAuthenticationMethod performNegotiation(final Timeout timeout) throws IOException {
+ SocksAuthenticationMethod[] authenticationMethods = getSocksAuthenticationMethods();
+
+ int methodsCount = authenticationMethods.length;
+
+ byte[] bufferSent = new byte[2 + methodsCount];
+ bufferSent[0] = SOCKS_VERSION;
+ bufferSent[1] = (byte) methodsCount;
+ for (int i = 0; i < methodsCount; i++) {
+ bufferSent[2 + i] = authenticationMethods[i].getMethodNumber();
+ }
+
+ OutputStream outputStream = getOutputStream();
+ outputStream.write(bufferSent);
+ outputStream.flush();
+
+ byte[] handshakeReply = readSocksReply(2, timeout);
+
+ if (handshakeReply[0] != SOCKS_VERSION) {
+ throw new ConnectException("Remote server doesn't support socks version 5"
+ + " Received version: " + handshakeReply[0]);
+ }
+ byte authMethodNumber = handshakeReply[1];
+ if (authMethodNumber == (byte) 0xFF) {
+ throw new ConnectException("None of the authentication methods listed are acceptable. Attempted methods: "
+ + Arrays.toString(authenticationMethods));
+ }
+ if (authMethodNumber == SocksAuthenticationMethod.NO_AUTH.getMethodNumber()) {
+ return SocksAuthenticationMethod.NO_AUTH;
+ } else if (authMethodNumber == SocksAuthenticationMethod.USERNAME_PASSWORD.getMethodNumber()) {
+ return SocksAuthenticationMethod.USERNAME_PASSWORD;
+ }
+
+ throw new ConnectException("Proxy returned unsupported authentication method: " + authMethodNumber);
+ }
+
+ private SocksAuthenticationMethod[] getSocksAuthenticationMethods() {
+ SocksAuthenticationMethod[] authMethods;
+ if (proxySettings.getUsername() != null) {
+ authMethods = new SocksAuthenticationMethod[]{
+ SocksAuthenticationMethod.NO_AUTH,
+ SocksAuthenticationMethod.USERNAME_PASSWORD};
+ } else {
+ authMethods = new SocksAuthenticationMethod[]{SocksAuthenticationMethod.NO_AUTH};
+ }
+ return authMethods;
+ }
+
+ private static Timeout toTimeout(final int timeoutMs) {
+ if (timeoutMs == 0) {
+ return Timeout.infinite();
+ }
+ return Timeout.startNow(timeoutMs, TimeUnit.MILLISECONDS);
+ }
+
+ private static int remainingMillis(final Timeout timeout) throws IOException {
+ if (timeout.isInfinite()) {
+ return 0;
+ }
+
+ final int remaining = Math.toIntExact(timeout.remaining(TimeUnit.MILLISECONDS));
+ if (remaining > 0) {
+ return remaining;
+ }
+
+ throw new SocketTimeoutException("Socket connection timed out");
+ }
+
+ private byte[] readSocksReply(final int length, final Timeout timeout) throws IOException {
+ InputStream inputStream = getInputStream();
+ byte[] data = new byte[length];
+ int received = 0;
+ int originalTimeout = getSoTimeout();
+ try {
+ while (received < length) {
+ int count;
+ int remaining = remainingMillis(timeout);
+ setSoTimeout(remaining);
+ count = inputStream.read(data, received, length - received);
+ if (count < 0) {
+ throw new ConnectException("Malformed reply from SOCKS proxy server");
+ }
+ received += count;
+ }
+ } finally {
+ setSoTimeout(originalTimeout);
+ }
+ return data;
+ }
+
+ enum SocksCommand {
+
+ CONNECT(0x01);
+
+ private final byte value;
+
+ SocksCommand(final int value) {
+ this.value = (byte) value;
+ }
+
+ public byte getCommandNumber() {
+ return value;
+ }
+ }
+
+ private enum SocksAuthenticationMethod {
+ NO_AUTH(0x00),
+ USERNAME_PASSWORD(0x02);
+
+ private final byte methodNumber;
+
+ SocksAuthenticationMethod(final int methodNumber) {
+ this.methodNumber = (byte) methodNumber;
+ }
+
+ public byte getMethodNumber() {
+ return methodNumber;
+ }
+ }
+
+ enum AddressType {
+ IP_V4(0x01, 4),
+ IP_V6(0x04, 16),
+ DOMAIN_NAME(0x03, -1) {
+ public byte getLength() {
+ throw fail();
+ }
+ };
+
+ private final byte length;
+ private final byte addressTypeNumber;
+
+ AddressType(final int addressTypeNumber, final int length) {
+ this.addressTypeNumber = (byte) addressTypeNumber;
+ this.length = (byte) length;
+ }
+
+ static AddressType of(final byte signedAddressType) throws ConnectException {
+ int addressTypeNumber = Byte.toUnsignedInt(signedAddressType);
+ for (AddressType addressType : AddressType.values()) {
+ if (addressTypeNumber == addressType.getAddressTypeNumber()) {
+ return addressType;
+ }
+ }
+ throw new ConnectException("Reply from SOCKS proxy server contains wrong address type"
+ + " Address type: " + addressTypeNumber);
+ }
+
+ byte getLength() {
+ return length;
+ }
+
+ byte getAddressTypeNumber() {
+ return addressTypeNumber;
+ }
+
+ }
+
+ enum ServerReply {
+ REPLY_SUCCEEDED(0x00, "Succeeded"),
+ GENERAL_FAILURE(0x01, "General SOCKS5 server failure"),
+ NOT_ALLOWED(0x02, "Connection is not allowed by ruleset"),
+ NET_UNREACHABLE(0x03, "Network is unreachable"),
+ HOST_UNREACHABLE(0x04, "Host is unreachable"),
+ CONN_REFUSED(0x05, "Connection has been refused"),
+ TTL_EXPIRED(0x06, "TTL is expired"),
+ CMD_NOT_SUPPORTED(0x07, "Command is not supported"),
+ ADDR_TYPE_NOT_SUP(0x08, "Address type is not supported");
+
+ private final int replyNumber;
+ private final String message;
+
+ ServerReply(final int replyNumber, final String message) {
+ this.replyNumber = replyNumber;
+ this.message = message;
+ }
+
+ static ServerReply of(final byte byteStatus) throws ConnectException {
+ int status = Byte.toUnsignedInt(byteStatus);
+ for (ServerReply serverReply : ServerReply.values()) {
+ if (status == serverReply.replyNumber) {
+ return serverReply;
+ }
+ }
+
+ throw new ConnectException("Unknown reply field. Reply field: " + status);
+ }
+
+ public String getMessage() {
+ return message;
+ }
+ }
+
+ @Override
+ @SuppressWarnings("try")
+ public void close() throws IOException {
+ /*
+ If this.socket is not null, this class essentially acts as a wrapper and we neither bind nor connect in the superclass,
+ nor do we get input/output streams from the superclass. While it might seem reasonable to skip calling super.close() in this case,
+ the Java SE Socket documentation doesn't definitively clarify this. Therefore, it's safer to always call super.close().
+ */
+ try (Socket autoClosed = socket) {
+ super.close();
+ }
+ }
+
+ @Override
+ public void setSoTimeout(final int timeout) throws SocketException {
+ if (socket != null) {
+ socket.setSoTimeout(timeout);
+ } else {
+ super.setSoTimeout(timeout);
+ }
+ }
+
+ @Override
+ public int getSoTimeout() throws SocketException {
+ if (socket != null) {
+ return socket.getSoTimeout();
+ } else {
+ return super.getSoTimeout();
+ }
+ }
+
+ @Override
+ public void bind(final SocketAddress bindpoint) throws IOException {
+ if (socket != null) {
+ socket.bind(bindpoint);
+ } else {
+ super.bind(bindpoint);
+ }
+ }
+
+ @Override
+ public InetAddress getInetAddress() {
+ if (socket != null) {
+ return socket.getInetAddress();
+ } else {
+ return super.getInetAddress();
+ }
+ }
+
+ @Override
+ public InetAddress getLocalAddress() {
+ if (socket != null) {
+ return socket.getLocalAddress();
+ } else {
+ return super.getLocalAddress();
+ }
+ }
+
+ @Override
+ public int getPort() {
+ if (socket != null) {
+ return socket.getPort();
+ } else {
+ return super.getPort();
+ }
+ }
+
+ @Override
+ public int getLocalPort() {
+ if (socket != null) {
+ return socket.getLocalPort();
+ } else {
+ return super.getLocalPort();
+ }
+ }
+
+ @Override
+ public SocketAddress getRemoteSocketAddress() {
+ if (socket != null) {
+ return socket.getRemoteSocketAddress();
+ } else {
+ return super.getRemoteSocketAddress();
+ }
+ }
+
+ @Override
+ public SocketAddress getLocalSocketAddress() {
+ if (socket != null) {
+ return socket.getLocalSocketAddress();
+ } else {
+ return super.getLocalSocketAddress();
+ }
+ }
+
+ @Override
+ public SocketChannel getChannel() {
+ if (socket != null) {
+ return socket.getChannel();
+ } else {
+ return super.getChannel();
+ }
+ }
+
+ @Override
+ public void setTcpNoDelay(final boolean on) throws SocketException {
+ if (socket != null) {
+ socket.setTcpNoDelay(on);
+ } else {
+ super.setTcpNoDelay(on);
+ }
+ }
+
+ @Override
+ public boolean getTcpNoDelay() throws SocketException {
+ if (socket != null) {
+ return socket.getTcpNoDelay();
+ } else {
+ return super.getTcpNoDelay();
+ }
+ }
+
+ @Override
+ public void setSoLinger(final boolean on, final int linger) throws SocketException {
+ if (socket != null) {
+ socket.setSoLinger(on, linger);
+ } else {
+ super.setSoLinger(on, linger);
+ }
+ }
+
+ @Override
+ public int getSoLinger() throws SocketException {
+ if (socket != null) {
+ return socket.getSoLinger();
+ } else {
+ return super.getSoLinger();
+ }
+ }
+
+ @Override
+ public void sendUrgentData(final int data) throws IOException {
+ if (socket != null) {
+ socket.sendUrgentData(data);
+ } else {
+ super.sendUrgentData(data);
+ }
+ }
+
+ @Override
+ public void setOOBInline(final boolean on) throws SocketException {
+ if (socket != null) {
+ socket.setOOBInline(on);
+ } else {
+ super.setOOBInline(on);
+ }
+ }
+
+ @Override
+ public boolean getOOBInline() throws SocketException {
+ if (socket != null) {
+ return socket.getOOBInline();
+ } else {
+ return super.getOOBInline();
+ }
+ }
+
+ @Override
+ public void setSendBufferSize(final int size) throws SocketException {
+ if (socket != null) {
+ socket.setSendBufferSize(size);
+ } else {
+ super.setSendBufferSize(size);
+ }
+ }
+
+ @Override
+ public int getSendBufferSize() throws SocketException {
+ if (socket != null) {
+ return socket.getSendBufferSize();
+ } else {
+ return super.getSendBufferSize();
+ }
+ }
+
+ @Override
+ public void setReceiveBufferSize(final int size) throws SocketException {
+ if (socket != null) {
+ socket.setReceiveBufferSize(size);
+ } else {
+ super.setReceiveBufferSize(size);
+ }
+ }
+
+ @Override
+ public int getReceiveBufferSize() throws SocketException {
+ if (socket != null) {
+ return socket.getReceiveBufferSize();
+ } else {
+ return super.getReceiveBufferSize();
+ }
+ }
+
+ @Override
+ public void setKeepAlive(final boolean on) throws SocketException {
+ if (socket != null) {
+ socket.setKeepAlive(on);
+ } else {
+ super.setKeepAlive(on);
+ }
+ }
+
+ @Override
+ public boolean getKeepAlive() throws SocketException {
+ if (socket != null) {
+ return socket.getKeepAlive();
+ } else {
+ return super.getKeepAlive();
+ }
+ }
+
+ @Override
+ public void setTrafficClass(final int tc) throws SocketException {
+ if (socket != null) {
+ socket.setTrafficClass(tc);
+ } else {
+ super.setTrafficClass(tc);
+ }
+ }
+
+ @Override
+ public int getTrafficClass() throws SocketException {
+ if (socket != null) {
+ return socket.getTrafficClass();
+ } else {
+ return super.getTrafficClass();
+ }
+ }
+
+ @Override
+ public void setReuseAddress(final boolean on) throws SocketException {
+ if (socket != null) {
+ socket.setReuseAddress(on);
+ } else {
+ super.setReuseAddress(on);
+ }
+ }
+
+ @Override
+ public boolean getReuseAddress() throws SocketException {
+ if (socket != null) {
+ return socket.getReuseAddress();
+ } else {
+ return super.getReuseAddress();
+ }
+ }
+
+ @Override
+ public void shutdownInput() throws IOException {
+ if (socket != null) {
+ socket.shutdownInput();
+ } else {
+ super.shutdownInput();
+ }
+ }
+
+ @Override
+ public void shutdownOutput() throws IOException {
+ if (socket != null) {
+ socket.shutdownOutput();
+ } else {
+ super.shutdownOutput();
+ }
+ }
+
+ @Override
+ public boolean isConnected() {
+ if (socket != null) {
+ return socket.isConnected();
+ } else {
+ return super.isConnected();
+ }
+ }
+
+ @Override
+ public boolean isBound() {
+ if (socket != null) {
+ return socket.isBound();
+ } else {
+ return super.isBound();
+ }
+ }
+
+ @Override
+ public boolean isClosed() {
+ if (socket != null) {
+ return socket.isClosed();
+ } else {
+ return super.isClosed();
+ }
+ }
+
+ @Override
+ public boolean isInputShutdown() {
+ if (socket != null) {
+ return socket.isInputShutdown();
+ } else {
+ return super.isInputShutdown();
+ }
+ }
+
+ @Override
+ public boolean isOutputShutdown() {
+ if (socket != null) {
+ return socket.isOutputShutdown();
+ } else {
+ return super.isOutputShutdown();
+ }
+ }
+
+ @Override
+ public void setPerformancePreferences(final int connectionTime, final int latency, final int bandwidth) {
+ if (socket != null) {
+ socket.setPerformancePreferences(connectionTime, latency, bandwidth);
+ } else {
+ super.setPerformancePreferences(connectionTime, latency, bandwidth);
+ }
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ if (socket != null) {
+ return socket.getInputStream();
+ }
+ return super.getInputStream();
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ if (socket != null) {
+ return socket.getOutputStream();
+ }
+ return super.getOutputStream();
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/SslHelper.java b/driver-core/src/main/com/mongodb/internal/connection/SslHelper.java
index d6d97549d3a..6e360b35b3f 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/SslHelper.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/SslHelper.java
@@ -16,9 +16,16 @@
package com.mongodb.internal.connection;
+import com.mongodb.MongoInternalException;
+import com.mongodb.connection.SslSettings;
+
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+
+import java.net.InetSocketAddress;
+import java.net.Socket;
import static java.util.Collections.singletonList;
@@ -51,6 +58,27 @@ public static void enableSni(final String host, final SSLParameters sslParameter
}
}
+ public static void configureSslSocket(final Socket socket, final SslSettings sslSettings, final InetSocketAddress inetSocketAddress) throws
+ MongoInternalException {
+ if (sslSettings.isEnabled() || socket instanceof SSLSocket) {
+ if (!(socket instanceof SSLSocket)) {
+ throw new MongoInternalException("SSL is enabled but the socket is not an instance of javax.net.ssl.SSLSocket");
+ }
+ SSLSocket sslSocket = (SSLSocket) socket;
+ SSLParameters sslParameters = sslSocket.getSSLParameters();
+ if (sslParameters == null) {
+ sslParameters = new SSLParameters();
+ }
+
+ enableSni(inetSocketAddress.getHostName(), sslParameters);
+
+ if (!sslSettings.isInvalidHostNameAllowed()) {
+ enableHostNameVerification(sslParameters);
+ }
+ sslSocket.setSSLParameters(sslParameters);
+ }
+ }
+
private SslHelper() {
}
}
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
new file mode 100644
index 00000000000..cc987cacf62
--- /dev/null
+++ b/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.internal.connection;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static com.mongodb.internal.connection.DomainNameUtils.isDomainName;
+
+class DomainNameUtilsTest {
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "hyphen-domain.com",
+ "sub.domain.com",
+ "sub.domain.c.com.com",
+ "123numbers.com",
+ "mixed-123domain.net",
+ "longdomainnameabcdefghijk.com",
+ "xn--frosch-6ya.com",
+ "xn--emoji-grinning-3s0b.org",
+ "xn--bcher-kva.ch",
+ "localhost",
+ "abcdefghijklmnopqrstuvwxyz0123456789-abcdefghijklmnopqrstuvwxyz.com",
+ "xn--weihnachten-uzb.org",
+ "sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain."
+ + "com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain."
+ + "com.domain.com.sub.domain.subb.com" //255 characters
+ })
+ void shouldReturnTrueWithValidHostName(final String hostname) {
+ Assertions.assertTrue(isDomainName(hostname));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "xn--tst-0qa.example",
+ "xn--frosch-6ya.w23",
+ "-special_chars_$$.net",
+ "special_chars_$$.net",
+ "special_chars_$$.123",
+ "subdomain..domain.com",
+ "_subdomain..domain.com",
+ "subdomain..domain._com",
+ "subdomain..domain.com_",
+ "notlocalhost",
+ "домен.com", //NON-ASCII
+ "ẞẞ.com", //NON-ASCII
+ "abcdefghijklmnopqrstuvwxyz0123456789-abcdefghijklmnopqrstuvwxyzl.com",
+ "this-domain-is-really-long-because-it-just-keeps-going-and-going-and-its-still-not-done-yet-because-theres-more.net",
+ "verylongsubdomainnamethatisreallylongandmaycausetroubleforparsing.example",
+ "sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain."
+ + "com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain.com.sub.domain."
+ + "com.sub.domain.com.domain.com.sub.domain.subbb.com" //256 characters
+ })
+ void shouldReturnFalseWithInvalidHostName(final String hostname) {
+ Assertions.assertFalse(isDomainName(hostname));
+ }
+}
diff --git a/driver-core/src/test/functional/com/mongodb/internal/connection/InetAddressUtilsTest.java b/driver-core/src/test/functional/com/mongodb/internal/connection/InetAddressUtilsTest.java
new file mode 100644
index 00000000000..6d26166ee25
--- /dev/null
+++ b/driver-core/src/test/functional/com/mongodb/internal/connection/InetAddressUtilsTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2008-present MongoDB, Inc.
+ * Copyright (C) 2008 The Guava Authors
+ *
+ * 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.internal.connection;
+
+
+import junit.framework.TestCase;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Tests for {@link InetAddressUtils}.
+ */
+public class InetAddressUtilsTest extends TestCase {
+ public void testForStringBogusInput() {
+ Set bogusInputs =
+ toSet(
+ "",
+ "016.016.016.016",
+ "016.016.016",
+ "016.016",
+ "016",
+ "000.000.000.000",
+ "000",
+ "0x0a.0x0a.0x0a.0x0a",
+ "0x0a.0x0a.0x0a",
+ "0x0a.0x0a",
+ "0x0a",
+ "42.42.42.42.42",
+ "42.42.42",
+ "42.42",
+ "42",
+ "42..42.42",
+ "42..42.42.42",
+ "42.42.42.42.",
+ "42.42.42.42...",
+ ".42.42.42.42",
+ ".42.42.42",
+ "...42.42.42.42",
+ "42.42.42.-0",
+ "42.42.42.+0",
+ ".",
+ "...",
+ "bogus",
+ "bogus.com",
+ "192.168.0.1.com",
+ "12345.67899.-54321.-98765",
+ "257.0.0.0",
+ "42.42.42.-42",
+ "42.42.42.ab",
+ "3ffe::1.net",
+ "3ffe::1::1",
+ "1::2::3::4:5",
+ "::7:6:5:4:3:2:", // should end with ":0"
+ ":6:5:4:3:2:1::", // should begin with "0:"
+ "2001::db:::1",
+ "FEDC:9878",
+ "+1.+2.+3.4",
+ "1.2.3.4e0",
+ "6:5:4:3:2:1:0", // too few parts
+ "::7:6:5:4:3:2:1:0", // too many parts
+ "7:6:5:4:3:2:1:0::", // too many parts
+ "9:8:7:6:5:4:3::2:1", // too many parts
+ "0:1:2:3::4:5:6:7", // :: must remove at least one 0.
+ "3ffe:0:0:0:0:0:0:0:1", // too many parts (9 instead of 8)
+ "3ffe::10000", // hextet exceeds 16 bits
+ "3ffe::goog",
+ "3ffe::-0",
+ "3ffe::+0",
+ "3ffe::-1",
+ ":",
+ ":::",
+ "::1.2.3",
+ "::1.2.3.4.5",
+ "::1.2.3.4:",
+ "1.2.3.4::",
+ "2001:db8::1:",
+ ":2001:db8::1",
+ ":1:2:3:4:5:6:7",
+ "1:2:3:4:5:6:7:",
+ ":1:2:3:4:5:6:");
+
+ for (String bogusInput : bogusInputs) {
+ try {
+ InetAddressUtils.forString(bogusInput);
+ fail("IllegalArgumentException expected for '" + bogusInput + "'");
+ } catch (IllegalArgumentException expected) {
+ }
+ assertFalse(InetAddressUtils.isInetAddress(bogusInput));
+ }
+ }
+
+ public void test3ff31() {
+ try {
+ InetAddressUtils.forString("3ffe:::1");
+ fail("IllegalArgumentException expected");
+ } catch (IllegalArgumentException expected) {
+ }
+ assertFalse(InetAddressUtils.isInetAddress("016.016.016.016"));
+ }
+
+ public void testForStringIPv4Input() throws UnknownHostException {
+ String ipStr = "192.168.0.1";
+ // Shouldn't hit DNS, because it's an IP string literal.
+ InetAddress ipv4Addr = InetAddress.getByName(ipStr);
+ assertEquals(ipv4Addr, InetAddressUtils.forString(ipStr));
+ assertTrue(InetAddressUtils.isInetAddress(ipStr));
+ }
+
+ public void testForStringIPv4NonAsciiInput() throws UnknownHostException {
+ String ipStr = "૧૯૨.૧૬૮.૦.૧"; // 192.168.0.1 in Gujarati digits
+ // Shouldn't hit DNS, because it's an IP string literal.
+ InetAddress ipv4Addr;
+ try {
+ ipv4Addr = InetAddress.getByName(ipStr);
+ } catch (UnknownHostException e) {
+ // OK: this is probably Android, which is stricter.
+ return;
+ }
+ assertEquals(ipv4Addr, InetAddressUtils.forString(ipStr));
+ assertTrue(InetAddressUtils.isInetAddress(ipStr));
+ }
+
+ public void testForStringIPv6Input() throws UnknownHostException {
+ String ipStr = "3ffe::1";
+ // Shouldn't hit DNS, because it's an IP string literal.
+ InetAddress ipv6Addr = InetAddress.getByName(ipStr);
+ assertEquals(ipv6Addr, InetAddressUtils.forString(ipStr));
+ assertTrue(InetAddressUtils.isInetAddress(ipStr));
+ }
+
+ public void testForStringIPv6NonAsciiInput() throws UnknownHostException {
+ String ipStr = "૩ffe::૧"; // 3ffe::1 with Gujarati digits for 3 and 1
+ // Shouldn't hit DNS, because it's an IP string literal.
+ InetAddress ipv6Addr;
+ try {
+ ipv6Addr = InetAddress.getByName(ipStr);
+ } catch (UnknownHostException e) {
+ // OK: this is probably Android, which is stricter.
+ return;
+ }
+ assertEquals(ipv6Addr, InetAddressUtils.forString(ipStr));
+ assertTrue(InetAddressUtils.isInetAddress(ipStr));
+ }
+
+ public void testForStringIPv6EightColons() throws UnknownHostException {
+ Set eightColons =
+ toSet("::7:6:5:4:3:2:1", "::7:6:5:4:3:2:0", "7:6:5:4:3:2:1::", "0:6:5:4:3:2:1::");
+
+ for (String ipString : eightColons) {
+ // Shouldn't hit DNS, because it's an IP string literal.
+ InetAddress ipv6Addr = InetAddress.getByName(ipString);
+ assertEquals(ipv6Addr, InetAddressUtils.forString(ipString));
+ assertTrue(InetAddressUtils.isInetAddress(ipString));
+ }
+ }
+
+ public void testConvertDottedQuadToHex() throws UnknownHostException {
+ Set ipStrings =
+ toSet("7::0.128.0.127", "7::0.128.0.128", "7::128.128.0.127", "7::0.128.128.127");
+
+ for (String ipString : ipStrings) {
+ // Shouldn't hit DNS, because it's an IP string literal.
+ InetAddress ipv6Addr = InetAddress.getByName(ipString);
+ assertEquals(ipv6Addr, InetAddressUtils.forString(ipString));
+ assertTrue(InetAddressUtils.isInetAddress(ipString));
+ }
+ }
+
+ // see https://github.com/google/guava/issues/2587
+ private static final Set SCOPE_IDS =
+ toSet("eno1", "en1", "eth0", "X", "1", "2", "14", "20");
+
+ public void testIPv4AddressWithScopeId() {
+ Set ipStrings = toSet("1.2.3.4", "192.168.0.1");
+ for (String ipString : ipStrings) {
+ for (String scopeId : SCOPE_IDS) {
+ String withScopeId = ipString + "%" + scopeId;
+ assertFalse(
+ "InetAddresses.isInetAddress(" + withScopeId + ") should be false but was true",
+ InetAddressUtils.isInetAddress(withScopeId));
+ }
+ }
+ }
+
+ private static Set toSet(final String... strings) {
+ return new HashSet<>(Arrays.asList(strings));
+ }
+
+ public void testDottedQuadAddressWithScopeId() {
+ Set ipStrings =
+ toSet("7::0.128.0.127", "7::0.128.0.128", "7::128.128.0.127", "7::0.128.128.127");
+ for (String ipString : ipStrings) {
+ for (String scopeId : SCOPE_IDS) {
+ String withScopeId = ipString + "%" + scopeId;
+ assertFalse(
+ "InetAddresses.isInetAddress(" + withScopeId + ") should be false but was true",
+ InetAddressUtils.isInetAddress(withScopeId));
+ }
+ }
+ }
+
+ public void testIPv6AddressWithScopeId() {
+ Set ipStrings =
+ toSet(
+ "0:0:0:0:0:0:0:1",
+ "fe80::a",
+ "fe80::1",
+ "fe80::2",
+ "fe80::42",
+ "fe80::3dd0:7f8e:57b7:34d5",
+ "fe80::71a3:2b00:ddd3:753f",
+ "fe80::8b2:d61e:e5c:b333",
+ "fe80::b059:65f4:e877:c40");
+ for (String ipString : ipStrings) {
+ for (String scopeId : SCOPE_IDS) {
+ String withScopeId = ipString + "%" + scopeId;
+ assertTrue(
+ "InetAddresses.isInetAddress(" + withScopeId + ") should be true but was false",
+ InetAddressUtils.isInetAddress(withScopeId));
+ assertEquals(InetAddressUtils.forString(withScopeId), InetAddressUtils.forString(ipString));
+ }
+ }
+ }
+}
diff --git a/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy b/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy
index 63cd8aeb27a..536a1e482e4 100644
--- a/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy
@@ -36,6 +36,7 @@ import static java.util.Arrays.asList
import static java.util.concurrent.TimeUnit.MILLISECONDS
class ConnectionStringSpecification extends Specification {
+ static final LONG_STRING = new String((1..256).collect { (byte) 1 } as byte[])
@Unroll
def 'should parse #connectionString into correct components'() {
@@ -377,6 +378,82 @@ class ConnectionStringSpecification extends Specification {
connectionString.getSslInvalidHostnameAllowed()
}
+ @Unroll
+ def 'should throw IllegalArgumentException when the proxy settings are invalid'() {
+ when:
+ new ConnectionString(connectionString)
+
+ then:
+ IllegalArgumentException exception = thrown(IllegalArgumentException)
+ assert exception.message == cause
+
+ where:
+ cause | connectionString
+ 'proxyPort can only be specified with proxyHost' | 'mongodb://localhost:27017/?proxyPort=1'
+ 'proxyPort should be within the valid range (0 to 65535)'| 'mongodb://localhost:27017/?proxyHost=a&proxyPort=-1'
+ 'proxyPort should be within the valid range (0 to 65535)'| 'mongodb://localhost:27017/?proxyHost=a&proxyPort=65536'
+ 'proxyUsername can only be specified with proxyHost' | 'mongodb://localhost:27017/?proxyUsername=1'
+ 'proxyUsername cannot be empty' | 'mongodb://localhost:27017/?proxyHost=a&proxyUsername='
+ 'proxyPassword can only be specified with proxyHost' | 'mongodb://localhost:27017/?proxyPassword=1'
+ 'proxyPassword cannot be empty' | 'mongodb://localhost:27017/?proxyHost=a&proxyPassword='
+ 'username\'s length in bytes cannot be greater than 255' | 'mongodb://localhost:27017/?proxyHost=a&proxyUsername=' + LONG_STRING
+ 'password\'s length in bytes cannot be greater than 255' | 'mongodb://localhost:27017/?proxyHost=a&proxyPassword=' + LONG_STRING
+ 'Both proxyUsername' +
+ ' and proxyPassword must be set together.' +
+ ' They cannot be set individually' | 'mongodb://localhost:27017/?proxyHost=a&proxyPassword=1'
+ }
+
+ @Unroll
+ def 'should create connection string with valid proxy socket settings'() {
+ when:
+ def connectionString = new ConnectionString(uri)
+
+ then:
+ assert connectionString.getProxyHost() == proxyHost
+ assert connectionString.getProxyPort() == 1081
+
+ where:
+ uri | proxyHost
+ 'mongodb://localhost:27017/?proxyHost=2001:db8:85a3::8a2e:370:7334&proxyPort=1081'| '2001:db8:85a3::8a2e:370:7334'
+ 'mongodb://localhost:27017/?proxyHost=::5000&proxyPort=1081' | '::5000'
+ 'mongodb://localhost:27017/?proxyHost=%3A%3A5000&proxyPort=1081' | '::5000'
+ 'mongodb://localhost:27017/?proxyHost=0::1&proxyPort=1081' | '0::1'
+ 'mongodb://localhost:27017/?proxyHost=hyphen-domain.com&proxyPort=1081' | 'hyphen-domain.com'
+ 'mongodb://localhost:27017/?proxyHost=sub.domain.c.com.com&proxyPort=1081' | 'sub.domain.c.com.com'
+ 'mongodb://localhost:27017/?proxyHost=192.168.0.1&proxyPort=1081' | '192.168.0.1'
+ }
+
+ @Unroll
+ def 'should create connection string with valid proxy credentials settings'() {
+ when:
+ def connectionString = new ConnectionString(uri)
+
+ then:
+ assert connectionString.getProxyPassword() == proxyPassword
+ assert connectionString.getProxyUsername() == proxyUsername
+
+ where:
+ uri | proxyPassword | proxyUsername
+ 'mongodb://localhost:27017/?proxyHost=test4&proxyPassword=pass%21wor%24&proxyUsername=user%21name'| 'pass!wor$' | 'user!name'
+ 'mongodb://localhost:27017/?proxyHost=::5000&proxyPassword=pass!wor$&proxyUsername=user!name' | 'pass!wor$' | 'user!name'
+ }
+
+ def 'should set proxy settings properties'() {
+ when:
+ def connectionString = new ConnectionString('mongodb+srv://test5.cc/?'
+ + 'proxyPort=1080'
+ + '&proxyHost=proxy.com'
+ + '&proxyUsername=username'
+ + '&proxyPassword=password')
+
+ then:
+ connectionString.getProxyHost() == 'proxy.com'
+ connectionString.getProxyPort() == 1080
+ connectionString.getProxyUsername() == 'username'
+ connectionString.getProxyPassword() == 'password'
+ }
+
+
@Unroll
def 'should throw IllegalArgumentException when the string #cause'() {
when:
@@ -619,6 +696,16 @@ class ConnectionStringSpecification extends Specification {
new ConnectionString('mongodb://ross:123@localhost/?'
+ 'authMechanism=SCRAM-SHA-1') | new ConnectionString('mongodb://ross:123@localhost/?'
+ 'authMechanism=SCRAM-SHA-1')
+ new ConnectionString('mongodb://ross:123@localhost/?'
+ + 'proxyHost=proxy.com'
+ + '&proxyPort=1080'
+ + '&proxyUsername=username'
+ + '&proxyPassword=password') | new ConnectionString('mongodb://ross:123@localhost/?'
+ + 'proxyHost=proxy.com'
+ + '&proxyPort=1080'
+ + '&proxyUsername=username'
+ + '&proxyPassword=password')
+
new ConnectionString('mongodb://localhost/db.coll'
+ '?minPoolSize=5;'
+ 'maxPoolSize=10;'
@@ -670,8 +757,19 @@ class ConnectionStringSpecification extends Specification {
+ '&readPreferenceTags='
+ '&maxConnecting=2')
new ConnectionString('mongodb://ross:123@localhost/?'
- + 'authMechanism=SCRAM-SHA-1') | new ConnectionString('mongodb://ross:123@localhost/?'
+ + 'authMechanism=SCRAM-SHA-1') | new ConnectionString('mongodb://ross:123@localhost/?'
+ 'authMechanism=GSSAPI')
+ new ConnectionString('mongodb://ross:123@localhost/?'
+ + 'proxyHost=proxy.com') | new ConnectionString('mongodb://ross:123@localhost/?'
+ + 'proxyHost=1proxy.com')
+ new ConnectionString('mongodb://ross:123@localhost/?'
+ + 'proxyHost=proxy.com&proxyPort=1080') | new ConnectionString('mongodb://ross:123@localhost/?'
+ + 'proxyHost=proxy.com1.com&proxyPort=1081')
+ new ConnectionString('mongodb://ross:123@localhost/?'
+ + 'proxyHost=proxy.com&proxyPassword=password'
+ + '&proxyUsername=username') | new ConnectionString('mongodb://ross:123@localhost/?'
+ + 'proxyHost=proxy.com&proxyPassword=password1'
+ + '&proxyUsername=username')
}
def 'should recognize SRV protocol'() {
diff --git a/driver-core/src/test/unit/com/mongodb/MongoClientSettingsSpecification.groovy b/driver-core/src/test/unit/com/mongodb/MongoClientSettingsSpecification.groovy
index 4cbee308462..be63708ddf0 100644
--- a/driver-core/src/test/unit/com/mongodb/MongoClientSettingsSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/MongoClientSettingsSpecification.groovy
@@ -19,6 +19,7 @@ package com.mongodb
import com.mongodb.connection.ClusterConnectionMode
import com.mongodb.connection.ClusterSettings
import com.mongodb.connection.ConnectionPoolSettings
+import com.mongodb.connection.ProxySettings
import com.mongodb.connection.ServerSettings
import com.mongodb.connection.SocketSettings
import com.mongodb.connection.SslSettings
@@ -53,6 +54,7 @@ class MongoClientSettingsSpecification extends Specification {
settings.clusterSettings == ClusterSettings.builder().build()
settings.connectionPoolSettings == ConnectionPoolSettings.builder().build()
settings.socketSettings == SocketSettings.builder().build()
+ settings.socketSettings.proxySettings == ProxySettings.builder().build()
settings.heartbeatSocketSettings == SocketSettings.builder().readTimeout(10000, TimeUnit.MILLISECONDS).build()
settings.serverSettings == ServerSettings.builder().build()
settings.streamFactoryFactory == null
@@ -306,6 +308,10 @@ class MongoClientSettingsSpecification extends Specification {
+ '&readConcernLevel=majority'
+ '&compressors=zlib&zlibCompressionLevel=5'
+ '&uuidRepresentation=standard'
+ + '&proxyHost=proxy.com'
+ + '&proxyPort=1080'
+ + '&proxyUsername=username'
+ + '&proxyPassword=password'
)
MongoClientSettings settings = MongoClientSettings.builder().applyConnectionString(connectionString).build()
MongoClientSettings expected = MongoClientSettings.builder()
@@ -340,6 +346,12 @@ class MongoClientSettingsSpecification extends Specification {
void apply(final SocketSettings.Builder builder) {
builder.connectTimeout(2500, TimeUnit.MILLISECONDS)
.readTimeout(5500, TimeUnit.MILLISECONDS)
+ .applyToProxySettings {
+ it.host('proxy.com')
+ it.port(1080)
+ it.username('username')
+ it.password('password')
+ }
}
})
.applyToSslSettings(new Block() {
@@ -397,6 +409,12 @@ class MongoClientSettingsSpecification extends Specification {
void apply(final SocketSettings.Builder builder) {
builder.connectTimeout(2500, TimeUnit.MILLISECONDS)
.readTimeout(5500, TimeUnit.MILLISECONDS)
+ .applyToProxySettings {
+ it.host('proxy.com')
+ it.port(1080)
+ it.username('username')
+ it.password('password')
+ }
}
})
.applyToSslSettings(new Block() {
@@ -448,6 +466,31 @@ class MongoClientSettingsSpecification extends Specification {
.build()
}
+ def 'should use the proxy settings for the heartbeat settings'() {
+ when:
+ def settings = MongoClientSettings.builder().applyToSocketSettings { SocketSettings.Builder builder ->
+ builder.connectTimeout(42, TimeUnit.SECONDS)
+ .readTimeout(60, TimeUnit.SECONDS)
+ .applyToProxySettings {
+ it.host('proxy.com')
+ it.port(1080)
+ it.username('username')
+ it.password('password')
+ }
+ }.build()
+
+ then:
+ settings.getHeartbeatSocketSettings() == SocketSettings.builder().connectTimeout(42, TimeUnit.SECONDS)
+ .readTimeout(42, TimeUnit.SECONDS)
+ .applyToProxySettings {
+ it.host('proxy.com')
+ it.port(1080)
+ it.username('username')
+ it.password('password')
+ }
+ .build()
+ }
+
def 'should use the configured heartbeat timeouts for the heartbeat settings'() {
when:
def settings = MongoClientSettings.builder()
diff --git a/driver-core/src/test/unit/com/mongodb/ProxySettingsTest.java b/driver-core/src/test/unit/com/mongodb/ProxySettingsTest.java
new file mode 100644
index 00000000000..e161b25b61c
--- /dev/null
+++ b/driver-core/src/test/unit/com/mongodb/ProxySettingsTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.connection.ProxySettings;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+class ProxySettingsTest {
+
+ private static final String PASSWORD = "password";
+ private static final String USERNAME = "username";
+ private static final String HOST = "proxy.example.com";
+ private static final int VALID_PORT = 1080;
+
+ static Stream shouldThrowExceptionWhenProxySettingsAreInInvalid() {
+ return Stream.of(
+ Arguments.of(ProxySettings.builder()
+ .port(VALID_PORT), "state should be: proxyPort can only be specified with proxyHost"),
+ Arguments.of(ProxySettings.builder()
+ .port(VALID_PORT)
+ .username(USERNAME)
+ .password(PASSWORD), "state should be: proxyPort can only be specified with proxyHost"),
+ Arguments.of(ProxySettings.builder()
+ .username(USERNAME), "state should be: proxyUsername can only be specified with proxyHost"),
+ Arguments.of(ProxySettings.builder()
+ .password(PASSWORD), "state should be: proxyPassword can only be specified with proxyHost"),
+ Arguments.of(ProxySettings.builder()
+ .host(HOST)
+ .username(USERNAME),
+ "state should be: Both proxyUsername and proxyPassword must be set together. They cannot be set individually"),
+ Arguments.of(ProxySettings.builder()
+ .host(HOST)
+ .password(PASSWORD),
+ "state should be: Both proxyUsername and proxyPassword must be set together. They cannot be set individually")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void shouldThrowExceptionWhenProxySettingsAreInInvalid(final ProxySettings.Builder builder, final String expectedErrorMessage) {
+ IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, builder::build);
+ Assertions.assertEquals(expectedErrorMessage, exception.getMessage());
+ }
+
+ static Stream shouldThrowExceptionWhenInvalidValueIsProvided() {
+ byte[] byteData = new byte[256];
+ Arrays.fill(byteData, (byte) 1);
+ return Stream.of(
+ Arguments.of((Executable) () -> ProxySettings.builder()
+ .port(-1), "state should be: proxyPort is within the valid range (0 to 65535)"),
+ Arguments.of((Executable) () -> ProxySettings.builder()
+ .port(65536), "state should be: proxyPort is within the valid range (0 to 65535)"),
+ Arguments.of((Executable) () -> ProxySettings.builder()
+ .host(""), "state should be: proxyHost is not empty"),
+ Arguments.of((Executable) () -> ProxySettings.builder()
+ .username(""), "state should be: username is not empty"),
+ Arguments.of((Executable) () -> ProxySettings.builder()
+ .username(new String(byteData)), "state should be: username's length in bytes is not greater than 255"),
+ Arguments.of((Executable) () -> ProxySettings.builder()
+ .password(""), "state should be: password is not empty"),
+ Arguments.of((Executable) () -> ProxySettings.builder()
+ .password(new String(byteData)), "state should be: password's length in bytes is not greater than 255"),
+ Arguments.of((Executable) () -> ProxySettings.builder()
+ .host(null), "proxyHost can not be null"),
+ Arguments.of((Executable) () -> ProxySettings.builder()
+ .username(null), "username can not be null"),
+ Arguments.of((Executable) () -> ProxySettings.builder()
+ .password(null), "password can not be null")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void shouldThrowExceptionWhenInvalidValueIsProvided(final Executable action, final String expectedMessage) {
+ IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, action);
+ Assertions.assertEquals(expectedMessage, exception.getMessage());
+ }
+
+ static Stream shouldNotThrowExceptionWhenProxySettingAreValid() {
+ return Stream.of(
+ Arguments.of(ProxySettings.builder()
+ .host(HOST)
+ .port(VALID_PORT)),
+ Arguments.of(ProxySettings.builder()
+ .host(HOST)),
+ Arguments.of(ProxySettings.builder()
+ .host(HOST)
+ .port(VALID_PORT)
+ .host(USERNAME)
+ .host(PASSWORD)),
+ Arguments.of(ProxySettings.builder()
+ .host(HOST)
+ .host(USERNAME)
+ .host(PASSWORD))
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void shouldNotThrowExceptionWhenProxySettingAreValid(final ProxySettings.Builder builder) {
+ builder.build();
+ }
+
+ @Test
+ void shouldGetExpectedValues() {
+ //given
+ ProxySettings proxySettings = ProxySettings.builder()
+ .host(HOST)
+ .port(VALID_PORT)
+ .username(USERNAME)
+ .password(PASSWORD)
+ .build();
+
+ Assertions.assertEquals(HOST, proxySettings.getHost());
+ Assertions.assertEquals(VALID_PORT, proxySettings.getPort());
+ Assertions.assertEquals(USERNAME, proxySettings.getUsername());
+ Assertions.assertEquals(PASSWORD, proxySettings.getPassword());
+ }
+}
diff --git a/driver-core/src/test/unit/com/mongodb/connection/SocketSettingsSpecification.groovy b/driver-core/src/test/unit/com/mongodb/connection/SocketSettingsSpecification.groovy
index 09eedbdc323..d46eb5c298f 100644
--- a/driver-core/src/test/unit/com/mongodb/connection/SocketSettingsSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/connection/SocketSettingsSpecification.groovy
@@ -33,6 +33,7 @@ class SocketSettingsSpecification extends Specification {
settings.getReadTimeout(MILLISECONDS) == 0
settings.receiveBufferSize == 0
settings.sendBufferSize == 0
+ settings.proxySettings == ProxySettings.builder().build()
}
def 'should set settings'() {
@@ -42,6 +43,12 @@ class SocketSettingsSpecification extends Specification {
.readTimeout(2000, MILLISECONDS)
.sendBufferSize(1000)
.receiveBufferSize(1500)
+ .applyToProxySettings {
+ it.host('proxy.com')
+ it.port(1080)
+ it.username('username')
+ it.password('password')
+ }
.build()
@@ -50,6 +57,11 @@ class SocketSettingsSpecification extends Specification {
settings.getReadTimeout(MILLISECONDS) == 2000
settings.sendBufferSize == 1000
settings.receiveBufferSize == 1500
+ def proxySettings = settings.getProxySettings()
+ proxySettings.getHost() == 'proxy.com'
+ proxySettings.getPort() == 1080
+ proxySettings.getUsername() == 'username'
+ proxySettings.getPassword() == 'password'
}
def 'should apply builder settings'() {
@@ -59,6 +71,12 @@ class SocketSettingsSpecification extends Specification {
.readTimeout(2000, MILLISECONDS)
.sendBufferSize(1000)
.receiveBufferSize(1500)
+ .applyToProxySettings {
+ it.host('proxy.com')
+ it.port(1080)
+ it.username('username')
+ it.password('password')
+ }
.build()
def settings = SocketSettings.builder(original).build()
@@ -68,13 +86,22 @@ class SocketSettingsSpecification extends Specification {
settings.getReadTimeout(MILLISECONDS) == 2000
settings.sendBufferSize == 1000
settings.receiveBufferSize == 1500
+ def proxySettings = settings.getProxySettings()
+ proxySettings.getHost() == 'proxy.com'
+ proxySettings.getPort() == 1080
+ proxySettings.getUsername() == 'username'
+ proxySettings.getPassword() == 'password'
}
def 'should apply connection string'() {
when:
def settings = SocketSettings.builder()
.applyConnectionString(new ConnectionString
- ('mongodb://localhost/?connectTimeoutMS=5000&socketTimeoutMS=2000'))
+ ('mongodb://localhost/?connectTimeoutMS=5000&socketTimeoutMS=2000'
+ + '&proxyHost=proxy.com'
+ + '&proxyPort=1080'
+ + '&proxyUsername=username'
+ + '&proxyPassword=password'))
.build()
@@ -83,6 +110,11 @@ class SocketSettingsSpecification extends Specification {
settings.getReadTimeout(MILLISECONDS) == 2000
settings.sendBufferSize == 0
settings.receiveBufferSize == 0
+ def proxySettings = settings.getProxySettings()
+ proxySettings.getHost() == 'proxy.com'
+ proxySettings.getPort() == 1080
+ proxySettings.getUsername() == 'username'
+ proxySettings.getPassword() == 'password'
}
def 'should apply settings'() {
@@ -93,6 +125,12 @@ class SocketSettingsSpecification extends Specification {
.readTimeout(2000, MILLISECONDS)
.sendBufferSize(1000)
.receiveBufferSize(1500)
+ .applyToProxySettings {
+ it.host('proxy.com')
+ it.port(1080)
+ it.username('username')
+ it.password('password')
+ }
.build()
expect:
@@ -108,12 +146,24 @@ class SocketSettingsSpecification extends Specification {
.readTimeout(2000, MILLISECONDS)
.sendBufferSize(1000)
.receiveBufferSize(1500)
+ .applyToProxySettings {
+ it.host('proxy.com')
+ it.port(1080)
+ it.username('username')
+ it.password('password')
+ }
.build() ==
SocketSettings.builder()
.connectTimeout(5000, MILLISECONDS)
.readTimeout(2000, MILLISECONDS)
.sendBufferSize(1000)
.receiveBufferSize(1500)
+ .applyToProxySettings {
+ it.host('proxy.com')
+ it.port(1080)
+ it.username('username')
+ it.password('password')
+ }
.build()
}
@@ -130,12 +180,24 @@ class SocketSettingsSpecification extends Specification {
.readTimeout(2000, MILLISECONDS)
.sendBufferSize(1000)
.receiveBufferSize(1500)
+ .applyToProxySettings {
+ it.host('proxy.com')
+ it.port(1080)
+ it.username('username')
+ it.password('password')
+ }
.build().hashCode() ==
SocketSettings.builder()
.connectTimeout(5000, MILLISECONDS)
.readTimeout(2000, MILLISECONDS)
.sendBufferSize(1000)
.receiveBufferSize(1500)
+ .applyToProxySettings {
+ it.host('proxy.com')
+ it.port(1080)
+ it.username('username')
+ it.password('password')
+ }
.build().hashCode()
}
diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/MongoClients.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/MongoClients.java
index 569b93083e6..2e34af751e1 100644
--- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/MongoClients.java
+++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/MongoClients.java
@@ -17,6 +17,7 @@
package com.mongodb.reactivestreams.client;
import com.mongodb.ConnectionString;
+import com.mongodb.MongoClientException;
import com.mongodb.MongoClientSettings;
import com.mongodb.MongoDriverInformation;
import com.mongodb.MongoInternalException;
@@ -109,6 +110,9 @@ public static MongoClient create(final MongoClientSettings settings) {
* @since 1.8
*/
public static MongoClient create(final MongoClientSettings settings, @Nullable final MongoDriverInformation mongoDriverInformation) {
+ if (settings.getSocketSettings().getProxySettings().isProxyEnabled()) {
+ throw new MongoClientException("Proxy is not supported for reactive clients");
+ }
if (settings.getStreamFactoryFactory() == null) {
if (settings.getSslSettings().isEnabled()) {
return createWithTlsChannel(settings, mongoDriverInformation);
diff --git a/driver-scala/src/main/scala/org/mongodb/scala/connection/ProxySettings.scala b/driver-scala/src/main/scala/org/mongodb/scala/connection/ProxySettings.scala
new file mode 100644
index 00000000000..3337d742dde
--- /dev/null
+++ b/driver-scala/src/main/scala/org/mongodb/scala/connection/ProxySettings.scala
@@ -0,0 +1,49 @@
+/*
+ * 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 org.mongodb.scala.connection
+
+import com.mongodb.connection.{ ProxySettings => JProxySettings }
+
+/**
+ * This setting is only applicable when communicating with a MongoDB server using the synchronous variant of `MongoClient`.
+ *
+ * This setting is furthermore ignored if:
+ *
+ * - the communication is via `com.mongodb.UnixServerAddress` (Unix domain socket).
+ * - a `StreamFactoryFactory` is `MongoClientSettings.Builder.streamFactoryFactory` configured.
+ *
+ *
+ * @see [[org.mongodb.scala.connection.SocketSettings]]
+ * @see [[org.mongodb.scala.AutoEncryptionSettings]]
+ * @see [[org.mongodb.scala.ClientEncryptionSettings]]
+ * @since 4.11
+ */
+object ProxySettings {
+
+ /**
+ * Creates a builder for ProxySettings.
+ *
+ * @return a new Builder for creating ProxySettings.
+ */
+ def builder(): Builder = JProxySettings.builder()
+
+ /**
+ * ProxySettings builder type
+ */
+ type Builder = JProxySettings.Builder
+
+}
diff --git a/driver-scala/src/main/scala/org/mongodb/scala/connection/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/connection/package.scala
index 7a7e26c56d2..ab0d39d2778 100644
--- a/driver-scala/src/main/scala/org/mongodb/scala/connection/package.scala
+++ b/driver-scala/src/main/scala/org/mongodb/scala/connection/package.scala
@@ -41,6 +41,22 @@ package object connection {
*/
type SocketSettings = com.mongodb.connection.SocketSettings
+ /**
+ * This setting is only applicable when communicating with a MongoDB server using the synchronous variant of `MongoClient`.
+ *
+ * This setting is furthermore ignored if:
+ *
+ * - the communication is via `com.mongodb.UnixServerAddress` (Unix domain socket).
+ * - a `StreamFactoryFactory` is `MongoClientSettings.Builder.streamFactoryFactory` configured.
+ *
+ *
+ * @see [[org.mongodb.scala.connection.SocketSettings]]
+ * @see [[org.mongodb.scala.AutoEncryptionSettings]]
+ * @see [[org.mongodb.scala.ClientEncryptionSettings]]
+ * @since 4.11
+ */
+ type ProxySettings = com.mongodb.connection.ProxySettings
+
/**
* Settings for connecting to MongoDB via SSL.
*/
diff --git a/driver-scala/src/test/scala/org/mongodb/scala/MongoClientSettingsSpec.scala b/driver-scala/src/test/scala/org/mongodb/scala/MongoClientSettingsSpec.scala
index 81eb55cfad3..3a25d3d5518 100644
--- a/driver-scala/src/test/scala/org/mongodb/scala/MongoClientSettingsSpec.scala
+++ b/driver-scala/src/test/scala/org/mongodb/scala/MongoClientSettingsSpec.scala
@@ -53,7 +53,11 @@ class MongoClientSettingsSpec extends BaseSpec {
override def apply(t: ServerSettings.Builder): Unit = {}
})
.applyToSocketSettings(new Block[SocketSettings.Builder] {
- override def apply(t: SocketSettings.Builder): Unit = {}
+ override def apply(t: SocketSettings.Builder): Unit = {
+ t.applyToProxySettings(new Block[ProxySettings.Builder] {
+ override def apply(t: ProxySettings.Builder): Unit = {}
+ })
+ }
})
.applyToSslSettings(new Block[SslSettings.Builder] {
override def apply(t: SslSettings.Builder): Unit = {}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java
new file mode 100644
index 00000000000..7beb11ee5f9
--- /dev/null
+++ b/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java
@@ -0,0 +1,199 @@
+/*
+ * 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.client;
+
+import com.mongodb.ConnectionString;
+import com.mongodb.MongoClientSettings;
+import com.mongodb.MongoSocketOpenException;
+import com.mongodb.MongoTimeoutException;
+import com.mongodb.connection.ClusterDescription;
+import com.mongodb.connection.ServerDescription;
+import com.mongodb.event.ClusterDescriptionChangedEvent;
+import com.mongodb.event.ClusterListener;
+import org.bson.Document;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.condition.DisabledIf;
+import org.junit.jupiter.api.condition.EnabledIf;
+import org.junit.jupiter.api.extension.ConditionEvaluationResult;
+import org.junit.jupiter.api.extension.ExecutionCondition;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.lang.String.format;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+import static org.mockito.Mockito.atLeast;
+
+/**
+ * See https://github.com/mongodb/specifications/blob/master/source/socks5-support/tests/README.rst#prose-tests
+ */
+@ExtendWith(Socks5ProseTest.SocksProxyPropertyCondition.class)
+class Socks5ProseTest {
+ private static final String MONGO_REPLICA_SET_URI_PREFIX = System.getProperty("org.mongodb.test.uri");
+ private static final String MONGO_SINGLE_MAPPED_URI_PREFIX = System.getProperty("org.mongodb.test.uri.singleHost");
+ private static final int PROXY_PORT = Integer.parseInt(System.getProperty("org.mongodb.test.uri.proxyPort"));
+ private MongoClient mongoClient;
+
+ @AfterEach
+ void tearDown() {
+ if (mongoClient != null) {
+ mongoClient.close();
+ }
+ }
+
+ static Stream noAuthConnectionStrings() {
+ return Stream.of(buildConnectionString(MONGO_SINGLE_MAPPED_URI_PREFIX, "proxyHost=localhost&proxyPort=%d&directConnection=true"),
+ buildConnectionString(MONGO_REPLICA_SET_URI_PREFIX, "proxyHost=localhost&proxyPort=%d"));
+ }
+
+ static Stream invalidAuthConnectionStrings() {
+ return Stream.of(buildConnectionString(MONGO_SINGLE_MAPPED_URI_PREFIX,
+ "proxyHost=localhost&proxyPort=%d&proxyUsername=nonexistentuser&proxyPassword=badauth&directConnection=true"),
+ buildConnectionString(MONGO_REPLICA_SET_URI_PREFIX,
+ "proxyHost=localhost&proxyPort=%d&proxyUsername=nonexistentuser&proxyPassword=badauth"));
+ }
+
+ static Stream validAuthConnectionStrings() {
+ return Stream.of(buildConnectionString(MONGO_SINGLE_MAPPED_URI_PREFIX,
+ "proxyHost=localhost&proxyPort=%d&proxyUsername=username&proxyPassword=p4ssw0rd&directConnection=true"),
+ buildConnectionString(MONGO_REPLICA_SET_URI_PREFIX,
+ "proxyHost=localhost&proxyPort=%d&proxyUsername=username&proxyPassword=p4ssw0rd"));
+ }
+
+ @ParameterizedTest(name = "Should connect without authentication in connection string. ConnectionString: {0}")
+ @MethodSource({"noAuthConnectionStrings", "invalidAuthConnectionStrings"})
+ @DisabledIf("isAuthEnabled")
+ void shouldConnectWithoutAuth(final ConnectionString connectionString) {
+ mongoClient = MongoClients.create(connectionString);
+ runHelloCommand(mongoClient);
+ }
+
+ @ParameterizedTest(name = "Should connect without authentication in proxy settings. ConnectionString: {0}")
+ @MethodSource({"noAuthConnectionStrings", "invalidAuthConnectionStrings"})
+ @DisabledIf("isAuthEnabled")
+ void shouldConnectWithoutAuthInProxySettings(final ConnectionString connectionString) {
+ mongoClient = MongoClients.create(buildMongoClientSettings(connectionString));
+ runHelloCommand(mongoClient);
+ }
+
+ @ParameterizedTest(name = "Should not connect without valid authentication in connection string. ConnectionString: {0}")
+ @MethodSource({"noAuthConnectionStrings", "invalidAuthConnectionStrings"})
+ @EnabledIf("isAuthEnabled")
+ void shouldNotConnectWithoutAuth(final ConnectionString connectionString) {
+ ClusterListener clusterListener = Mockito.mock(ClusterListener.class);
+
+ mongoClient = createMongoClient(MongoClientSettings.builder()
+ .applyConnectionString(connectionString), clusterListener);
+
+ Assertions.assertThrows(MongoTimeoutException.class, () -> runHelloCommand(mongoClient));
+ assertSocksAuthenticationIssue(clusterListener);
+ }
+
+ @ParameterizedTest(name = "Should not connect without valid authentication in proxy settings. ConnectionString: {0}")
+ @MethodSource({"noAuthConnectionStrings", "invalidAuthConnectionStrings"})
+ @EnabledIf("isAuthEnabled")
+ void shouldNotConnectWithoutAuthInProxySettings(final ConnectionString connectionString) {
+ ClusterListener clusterListener = Mockito.mock(ClusterListener.class);
+
+ mongoClient = createMongoClient(MongoClientSettings.builder(buildMongoClientSettings(connectionString)), clusterListener);
+
+ Assertions.assertThrows(MongoTimeoutException.class, () -> runHelloCommand(mongoClient));
+ assertSocksAuthenticationIssue(clusterListener);
+ }
+
+ @ParameterizedTest(name = "Should connect with valid authentication in connection string. ConnectionString: {0}")
+ @MethodSource("validAuthConnectionStrings")
+ @EnabledIf("isAuthEnabled")
+ void shouldConnectWithValidAuth(final ConnectionString connectionString) {
+ mongoClient = MongoClients.create(connectionString);
+ runHelloCommand(mongoClient);
+ }
+
+ @ParameterizedTest(name = "Should connect with valid authentication in proxy settings. ConnectionString: {0}")
+ @MethodSource("validAuthConnectionStrings")
+ @EnabledIf("isAuthEnabled")
+ void shouldConnectWithValidAuthInProxySettings(final ConnectionString connectionString) {
+ mongoClient = MongoClients.create(buildMongoClientSettings(connectionString));
+ runHelloCommand(mongoClient);
+ }
+
+ private static void assertSocksAuthenticationIssue(final ClusterListener clusterListener) {
+ final ArgumentCaptor captor = ArgumentCaptor.forClass(ClusterDescriptionChangedEvent.class);
+ Mockito.verify(clusterListener, atLeast(1)).clusterDescriptionChanged(captor.capture());
+ List errors = captor.getAllValues().stream()
+ .map(ClusterDescriptionChangedEvent::getNewDescription)
+ .map(ClusterDescription::getServerDescriptions)
+ .flatMap(List::stream)
+ .map(ServerDescription::getException)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ assumeFalse(errors.isEmpty());
+ errors.forEach(throwable -> Assertions.assertEquals(MongoSocketOpenException.class, throwable.getClass()));
+ }
+
+ private static void runHelloCommand(final MongoClient mongoClient) {
+ mongoClient.getDatabase("test").runCommand(new Document("hello", 1));
+ }
+
+ private static ConnectionString buildConnectionString(final String uriPrefix, final String uriParameters) {
+ String format;
+ if (uriPrefix.contains("/?")) {
+ format = uriPrefix + "&" + uriParameters;
+ } else {
+ format = uriPrefix + "/?" + uriParameters;
+ }
+ return new ConnectionString(format(format, PROXY_PORT));
+ }
+
+ private static MongoClientSettings buildMongoClientSettings(final ConnectionString connectionString) {
+ return MongoClientSettings.builder().applyConnectionString(connectionString).build();
+ }
+
+ private static MongoClient createMongoClient(final MongoClientSettings.Builder settingsBuilder, final ClusterListener clusterListener) {
+ return MongoClients.create(settingsBuilder
+ .applyToClusterSettings(builder -> {
+ builder.addClusterListener(clusterListener);
+ // to speed up test execution in case of socks authentication issues. Default is 30 seconds.
+ builder.serverSelectionTimeout(5, TimeUnit.SECONDS);
+ })
+ .build());
+ }
+
+ private static boolean isAuthEnabled() {
+ return Boolean.parseBoolean(System.getProperty("org.mongodb.test.uri.socks.auth.enabled"));
+ }
+
+ public static class SocksProxyPropertyCondition implements ExecutionCondition {
+ @Override
+ public ConditionEvaluationResult evaluateExecutionCondition(final ExtensionContext context) {
+ if (System.getProperty("org.mongodb.test.uri.socks.auth.enabled") != null) {
+ return ConditionEvaluationResult.enabled("Test is enabled because socks proxy configuration exists");
+ } else {
+ return ConditionEvaluationResult.disabled("Test is disabled because socks proxy configuration is missing");
+ }
+ }
+ }
+}