From 311515e2605527368b2c7fd613c821fc81527303 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 2 Aug 2023 20:45:13 -0700 Subject: [PATCH 01/27] Introduce SOCKS5 proxy support, allowing seamless connections to MongoDB instances behind firewalls. JAVA-4347 --- .evergreen/.evg.yml | 35 ++ .evergreen/run-socks5-tests.sh | 84 ++++ THIRD-PARTY-NOTICES | 20 + config/spotbugs/exclude.xml | 9 +- .../com/mongodb/AutoEncryptionSettings.java | 44 ++ .../com/mongodb/ClientEncryptionSettings.java | 38 ++ .../main/com/mongodb/ConnectionString.java | 68 +++ .../main/com/mongodb/MongoClientSettings.java | 1 + .../com/mongodb/connection/ProxySettings.java | 243 ++++++++++ .../mongodb/connection/SocketSettings.java | 46 +- .../internal/connection/InetAddresses.java | 322 +++++++++++++ .../internal/connection/SocketStream.java | 48 ++ .../connection/SocketStreamHelper.java | 31 +- .../internal/connection/SocksSocket.java | 444 ++++++++++++++++++ .../internal/connection/SslHelper.java | 28 ++ .../com/mongodb/internal/SocksSocketTest.java | 191 ++++++++ .../ConnectionStringSpecification.groovy | 23 + .../unit/com/mongodb/ProxySettingsTest.java | 82 ++++ .../reactivestreams/client/MongoClients.java | 4 + .../scala/connection/ProxySettings.scala | 41 ++ .../mongodb/scala/connection/package.scala | 6 + .../scala/MongoClientSettingsSpec.scala | 6 +- .../com/mongodb/client/internal/Crypts.java | 15 +- .../client/internal/KeyManagementService.java | 42 +- .../com/mongodb/client/Socks5ProseTest.java | 234 +++++++++ 25 files changed, 2060 insertions(+), 45 deletions(-) create mode 100644 .evergreen/run-socks5-tests.sh create mode 100644 driver-core/src/main/com/mongodb/connection/ProxySettings.java create mode 100644 driver-core/src/main/com/mongodb/internal/connection/InetAddresses.java create mode 100644 driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java create mode 100644 driver-core/src/test/functional/com/mongodb/internal/SocksSocketTest.java create mode 100644 driver-core/src/test/unit/com/mongodb/ProxySettingsTest.java create mode 100644 driver-scala/src/main/scala/org/mongodb/scala/connection/ProxySettings.scala create mode 100644 driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index 302be327499..154d7bf5c8b 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -728,6 +728,17 @@ functions: MONGODB_URIS="${atlas_free_tier_uri}|${atlas_replica_set_uri}|${atlas_sharded_uri}|${atlas_tls_v11_uri}|${atlas_tls_v12_uri}|${atlas_free_tier_uri_srv}|${atlas_replica_set_uri_srv}|${atlas_sharded_uri_srv}|${atlas_tls_v11_uri_srv}|${atlas_tls_v12_uri_srv}|${atlas_serverless_uri}|${atlas_serverless_uri_srv}" \ .evergreen/run-connectivity-tests.sh + run socks5 tests: + - command: shell.exec + type: test + params: + working_dir: src + script: | + ${PREPARE_SHELL} + SSL="${SSL}" MONGODB_URI="${MONGODB_URI}" \ + JAVA_VERSION="${JAVA_VERSION}" \ + .evergreen/run-socks5-tests.sh + start-kms-mock-server: - command: shell.exec params: @@ -1605,6 +1616,24 @@ tasks: export AZUREKMS_VMNAME=${AZUREKMS_VMNAME} export AZUREKMS_PRIVATEKEYPATH=/tmp/testazurekms_privatekey AZUREKMS_CMD="MONGODB_URI=mongodb://localhost:27017 PROVIDER=azure AZUREKMS_KEY_VAULT_ENDPOINT=${testazurekms_keyvaultendpoint} AZUREKMS_KEY_NAME=${testazurekms_keyname} ./.evergreen/run-fle-on-demand-credential-test.sh" $DRIVERS_TOOLS/.evergreen/csfle/azurekms/run-command.sh + - name: test-socks5 + tags: [] + commands: + - func: bootstrap mongo-orchestration + vars: + VERSION: latest + TOPOLOGY: replica_set + - func: run socks5 tests + - name: test-socks5-tls + tags: [] + commands: + - func: bootstrap mongo-orchestration + vars: + VERSION: latest + TOPOLOGY: replica_set + - func: run socks5 tests + vars: + SSL: ssl axes: - id: version display_name: MongoDB Version @@ -2136,6 +2165,12 @@ buildvariants: tasks: - name: "csfle-tests-with-mongocryptd" +- matrix_name: "socks5-tests" + matrix_spec: { os: "linux", ssl: ["nossl", "ssl"], version: [ "latest" ], topology: ["replicaset"] } + display_name: "Socks5: ${version} ${topology} ${ssl} ${jdk} ${os}" + tasks: + - name: test-socks5 + - name: testgcpkms-variant display_name: "GCP KMS" run_on: diff --git a/.evergreen/run-socks5-tests.sh b/.evergreen/run-socks5-tests.sh new file mode 100644 index 00000000000..92e1b4a169c --- /dev/null +++ b/.evergreen/run-socks5-tests.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +set -o xtrace # Write all commands first to stderr +set -o errexit # Exit the script with error if any of the commands fail + +SSL=${SSL:-nossl} +MONGODB_URI=${MONGODB_URI:-} +SOCKS5_SERVER_SCRIPT="$DRIVERS_TOOLS/.evergreen/socks5srv.py" +PYTHON_BINARY=${PYTHON_BINARY:-python3} +# Grab a connection string that only refers to *one* of the hosts in MONGODB_URI +FIRST_HOST=$(echo "$MONGODB_URI" | awk -F[/:,] '{print $4":"$5}') +# Use 127.0.0.1:12345 as the URL for the single host that we connect to, +# we configure the Socks5 proxy server script to redirect from this to FIRST_HOST +export MONGODB_URI_SINGLEHOST="mongodb://127.0.0.1:12345" + +if [ "${SSL}" = "ssl" ]; then + MONGODB_URI="${MONGODB_URI}&ssl=true&sslInvalidHostNameAllowed=true" + MONGODB_URI_SINGLEHOST="${MONGODB_URI_SINGLEHOST}/?ssl=true&sslInvalidHostNameAllowed=true" +fi + +# Compute path to socks5 fake server script in a way that works on Windows +if [ "Windows_NT" == "$OS" ]; then + SOCKS5_SERVER_SCRIPT=$(cygpath -m $DRIVERS_TOOLS) +fi + +RELATIVE_DIR_PATH="$(dirname "${BASH_SOURCE:-$0}")" +. "${RELATIVE_DIR_PATH}/javaConfig.bash" + +############################################ +# Functions # +############################################ + +provision_ssl () { + # We generate the keystore and truststore on every run with the certs in the drivers-tools repo + if [ ! -f client.pkc ]; then + openssl pkcs12 -CAfile ${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem -export -in ${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem -out client.pkc -password pass:bithere + fi + + cp ${JAVA_HOME}/lib/security/cacerts mongo-truststore + ${JAVA_HOME}/bin/keytool -importcert -trustcacerts -file ${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem -keystore mongo-truststore -storepass changeit -storetype JKS -noprompt + + # We add extra gradle arguments for SSL + export GRADLE_SSL_VARS="-Pssl.enabled=true -Pssl.keyStoreType=pkcs12 -Pssl.keyStore=`pwd`/client.pkc -Pssl.keyStorePassword=bithere -Pssl.trustStoreType=jks -Pssl.trustStore=`pwd`/mongo-truststore -Pssl.trustStorePassword=changeit" +} + +run_socks5_prose_tests () { +local proxyPort=$1 +local authEnabled=$2 +./gradlew -PjavaVersion=${JAVA_VERSION} -Dorg.mongodb.test.uri=${MONGODB_URI} \ + -Dorg.mongodb.test.uri.singleHost=${MONGODB_URI_SINGLEHOST} \ + -Dorg.mongodb.test.uri.proxyHost="127.0.0.1" \ + -Dorg.mongodb.test.uri.proxyPort=${proxyPort} \ + -Dorg.mongodb.test.uri.socks.auth.enabled=${authEnabled} \ + ${GRADLE_SSL_VARS} \ + --stacktrace --info --continue \ + driver-sync:test \ + --tests "*.Socks5ProseTest*" +} + +############################################ +# Main Program # +############################################ + +# Set up keystore/truststore +if [ "${SSL}" = "ssl" ]; then + provision_ssl +fi + +# First, test with Socks5 + authentication required +echo "Running tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connecting to $MONGODB_URI with socks5 auth enabled" +./gradlew -version +"$PYTHON_BINARY" "$SOCKS5_SERVER_SCRIPT" --port 1080 --auth username:p4ssw0rd --map "127.0.0.1:12345 to $FIRST_HOST" & +SOCKS5_SERVER_PID_1=$! +trap "kill $SOCKS5_SERVER_PID_1" EXIT +run_socks5_prose_tests "1080" "true" + +# Second, test with Socks5 + no authentication +echo "Running tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connecting to $MONGODB_URI with socks5 auth disabled" +./gradlew -version +"$PYTHON_BINARY" "$SOCKS5_SERVER_SCRIPT" --port 1081 --map "127.0.0.1:12345 to $FIRST_HOST" & +# Set up trap to kill both processes when the script exits +SOCKS5_SERVER_PID_2=$! +trap "kill $SOCKS5_SERVER_PID_1; kill $SOCKS5_SERVER_PID_2" EXIT +run_socks5_prose_tests "1081" "false" \ No newline at end of file diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index e13f724e349..6fead4d514c 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -154,3 +154,23 @@ https://github.com/mongodb/mongo-java-driver. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +8) The following files (originally from https://github.com/google/guava): + + InetAddresses.java.java + + 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. + diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index 96d7695af2b..43db87eb91b 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -192,10 +192,17 @@ - + + + + + + + + diff --git a/driver-core/src/main/com/mongodb/AutoEncryptionSettings.java b/driver-core/src/main/com/mongodb/AutoEncryptionSettings.java index 1e2be618150..9c570dc0b4b 100644 --- a/driver-core/src/main/com/mongodb/AutoEncryptionSettings.java +++ b/driver-core/src/main/com/mongodb/AutoEncryptionSettings.java @@ -17,6 +17,8 @@ package com.mongodb; import com.mongodb.annotations.NotThreadSafe; +import com.mongodb.connection.ProxySettings; +import com.mongodb.connection.SocketSettings; import com.mongodb.lang.Nullable; import org.bson.BsonDocument; @@ -73,6 +75,14 @@ public final class AutoEncryptionSettings { private final boolean bypassAutoEncryption; private final Map encryptedFieldsMap; private final boolean bypassQueryAnalysis; + /** + * Proxy setting used for connecting to Key Management Service via a SOCKS5 proxy server. + *

+ * NOTE: If this field is left unset, the default behavior is to utilize the {@link SocketSettings#getProxySettings()} + * from the {@link MongoClientSettings} where the {@link AutoEncryptionSettings} are specified. + */ + @Nullable + private final ProxySettings proxySettings; /** * A builder for {@code AutoEncryptionSettings} so that {@code AutoEncryptionSettings} can be immutable, and to support easier @@ -90,6 +100,10 @@ public static final class Builder { private boolean bypassAutoEncryption; private Map encryptedFieldsMap = Collections.emptyMap(); private boolean bypassQueryAnalysis; + /** + * Proxy setting used for connecting to Key Management Service via a SOCKS5 proxy server. + */ + private ProxySettings proxySettings; /** * Sets the key vault settings. @@ -103,6 +117,24 @@ public Builder keyVaultMongoClientSettings(final MongoClientSettings keyVaultMon return this; } + /** + * Sets the {@link ProxySettings} used for connecting to Key Management Service via a SOCKS5 proxy server. + * + *

+ * NOTE: If this field is left unset, the default behavior is to utilize the {@link SocketSettings#getProxySettings()} + * from the {@link MongoClientSettings} where the {@link AutoEncryptionSettings} are specified. + * + * @param proxySettings {@link ProxySettings} to set. + * @return this. + * @see #getProxySettings() + * @since 4.11 + */ + public Builder proxySettings(final ProxySettings proxySettings) { + notNull("proxySettings", proxySettings); + this.proxySettings = proxySettings; + return this; + } + /** * Sets the key vault namespace * @@ -275,6 +307,17 @@ public MongoClientSettings getKeyVaultMongoClientSettings() { return keyVaultMongoClientSettings; } + /** + * Gets the proxy settings used for connecting to Key Management Service via a SOCKS5 proxy server. + * + * @return The {@link ProxySettings} instance containing the SOCKS5 proxy configuration. + * @since 4.11 + */ + @Nullable + public ProxySettings getProxySettings() { + return proxySettings; + } + /** * Gets the key vault namespace. * @@ -493,6 +536,7 @@ private AutoEncryptionSettings(final Builder builder) { this.bypassAutoEncryption = builder.bypassAutoEncryption; this.encryptedFieldsMap = builder.encryptedFieldsMap; this.bypassQueryAnalysis = builder.bypassQueryAnalysis; + this.proxySettings = builder.proxySettings; } @Override diff --git a/driver-core/src/main/com/mongodb/ClientEncryptionSettings.java b/driver-core/src/main/com/mongodb/ClientEncryptionSettings.java index 2df4b3363d4..0d5f46967a3 100644 --- a/driver-core/src/main/com/mongodb/ClientEncryptionSettings.java +++ b/driver-core/src/main/com/mongodb/ClientEncryptionSettings.java @@ -17,6 +17,8 @@ package com.mongodb; import com.mongodb.annotations.NotThreadSafe; +import com.mongodb.connection.ProxySettings; +import com.mongodb.lang.Nullable; import javax.net.ssl.SSLContext; import java.util.HashMap; @@ -42,6 +44,12 @@ public final class ClientEncryptionSettings { private final Map> kmsProviders; private final Map>> kmsProviderPropertySuppliers; private final Map kmsProviderSslContextMap; + /** + * Proxy setting used for connecting to Key Management Service via a SOCKS5 proxy server. + */ + @Nullable + private final ProxySettings proxySettings; + /** * A builder for {@code ClientEncryptionSettings} so that {@code ClientEncryptionSettings} can be immutable, and to support easier * construction through chaining. @@ -53,6 +61,10 @@ public static final class Builder { private Map> kmsProviders; private Map>> kmsProviderPropertySuppliers = new HashMap<>(); private Map kmsProviderSslContextMap = new HashMap<>(); + /** + * Proxy setting used for connecting to Key Management Service via a SOCKS5 proxy server. + */ + private ProxySettings proxySettings; /** * Sets the {@link MongoClientSettings} that will be used to access the key vault. @@ -66,6 +78,20 @@ public Builder keyVaultMongoClientSettings(final MongoClientSettings keyVaultMon return this; } + /** + * Sets the {@link ProxySettings} used for connecting to Key Management Service via a SOCKS5 proxy server. + * + * @param proxySettings {@link ProxySettings} to set. + * @return this. + * @see #getProxySettings() + * @since 4.11 + */ + public Builder proxySettings(final ProxySettings proxySettings) { + notNull("proxySettings", proxySettings); + this.proxySettings = proxySettings; + return this; + } + /** * Sets the key vault namespace * @@ -151,6 +177,17 @@ public MongoClientSettings getKeyVaultMongoClientSettings() { return keyVaultMongoClientSettings; } + /** + * Gets the proxy settings used for connecting to Key Management Service via a SOCKS5 proxy server. + * + * @return The {@link ProxySettings} instance containing the SOCKS5 proxy configuration. + * @since 4.11 + */ + @Nullable + public ProxySettings getProxySettings() { + return proxySettings; + } + /** * Gets the key vault namespace. *

@@ -259,6 +296,7 @@ private ClientEncryptionSettings(final Builder builder) { this.kmsProviders = notNull("kmsProviders", builder.kmsProviders); this.kmsProviderPropertySuppliers = notNull("kmsProviderPropertySuppliers", builder.kmsProviderPropertySuppliers); this.kmsProviderSslContextMap = notNull("kmsProviderSslContextMap", builder.kmsProviderSslContextMap); + this.proxySettings = builder.proxySettings; } } diff --git a/driver-core/src/main/com/mongodb/ConnectionString.java b/driver-core/src/main/com/mongodb/ConnectionString.java index 116dc2fc9b1..beb1a825a4b 100644 --- a/driver-core/src/main/com/mongodb/ConnectionString.java +++ b/driver-core/src/main/com/mongodb/ConnectionString.java @@ -283,6 +283,10 @@ public class ConnectionString { private Integer socketTimeout; private Boolean sslEnabled; private Boolean sslInvalidHostnameAllowed; + private String proxyHost; + private Integer proxyPort; + private String proxyUsername; + private String proxyPassword; private String requiredReplicaSetName; private Integer serverSelectionTimeout; private Integer localThreshold; @@ -461,6 +465,8 @@ public ConnectionString(final String connectionString, @Nullable final DnsClient throw new IllegalArgumentException("srvMaxHosts can not be specified with replica set name"); } + validateProxyParameters(); + credential = createCredentials(combinedOptionsMaps, userName, password); warnOnUnsupportedOptions(combinedOptionsMaps); } @@ -495,6 +501,12 @@ public ConnectionString(final String connectionString, @Nullable final DnsClient GENERAL_OPTIONS_KEYS.add("sslinvalidhostnameallowed"); GENERAL_OPTIONS_KEYS.add("tlsallowinvalidhostnames"); + //Socks5 proxy settings + GENERAL_OPTIONS_KEYS.add("proxyhost"); + GENERAL_OPTIONS_KEYS.add("proxyport"); + GENERAL_OPTIONS_KEYS.add("proxyusername"); + GENERAL_OPTIONS_KEYS.add("proxypassword"); + GENERAL_OPTIONS_KEYS.add("replicaset"); GENERAL_OPTIONS_KEYS.add("readconcernlevel"); @@ -607,6 +619,18 @@ private void translateOptions(final Map> optionsMap) { case "ssl": initializeSslEnabled("ssl", value); break; + case "proxyhost": + proxyHost = value; + break; + case "proxyport": + proxyPort = parseInteger(value, "proxyPort"); + break; + case "proxyusername": + proxyUsername = value; + break; + case "proxypassword": + proxyPassword = value; + break; case "tls": initializeSslEnabled("tls", value); break; @@ -1151,6 +1175,31 @@ 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) { + throw new IllegalArgumentException("proxyPort should be equal or greater than 0"); + } + if (proxyUsername != null && proxyUsername.isEmpty()) { + throw new IllegalArgumentException("proxyUsername cannot be empty"); + } + if (proxyPassword != null && proxyPassword.isEmpty()) { + throw new IllegalArgumentException("proxyPassword cannot be empty"); + } + 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 +1488,25 @@ public Boolean getSslEnabled() { return sslEnabled; } + @Nullable + public String getProxyHost() { + return proxyHost; + } + + @Nullable + public Integer getProxyPort() { + return proxyPort; + } + + @Nullable + public String getProxyUsername() { + return proxyUsername; + } + + @Nullable + public String getProxyPassword() { + return proxyPassword; + } /** * Gets the SSL invalidHostnameAllowed value specified in the connection string. * 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..83257c7e065 --- /dev/null +++ b/driver-core/src/main/com/mongodb/connection/ProxySettings.java @@ -0,0 +1,243 @@ +/* + * 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.ConnectionString; +import com.mongodb.annotations.Immutable; +import com.mongodb.lang.Nullable; + +import java.util.Objects; + +import static com.mongodb.assertions.Assertions.isTrue; +import static com.mongodb.assertions.Assertions.notNull; + +/** + * An immutable class representing settings for connecting to MongoDB via a SOCKS5 proxy server. + * NOTE: This setting is only applicable to the synchronous variant of MongoClient and Key Management Service settings. + * + * @since 4.11 + */ +@Immutable +public final class ProxySettings { + + @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}. + * @since 4.11 + */ + 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}. + * @since 4.11 + */ + 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() { + } + + 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; + } + + + public ProxySettings.Builder host(final String host) { + notNull("proxyHost", host); + isTrue("proxyHost is not empty", host.trim().length() > 0); + this.host = host; + return this; + } + + public ProxySettings.Builder port(final int port) { + isTrue("proxyPort is equal or greater than 0", port >= 0); + this.port = port; + return this; + } + + public ProxySettings.Builder username(final String username) { + notNull("username", username); + isTrue("username is not empty", !username.isEmpty()); + this.username = username; + return this; + } + + public ProxySettings.Builder password(final String password) { + notNull("password", password); + isTrue("password is not empty", !password.isEmpty()); + 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); + } + } + + @Nullable + public String getHost() { + return host; + } + + @Nullable + public Integer getPort() { + return port; + } + + @Nullable + public String getUsername() { + return username; + } + + @Nullable + public String getPassword() { + return password; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ProxySettings)) { + return false; + } + + ProxySettings that = (ProxySettings) o; + + if (!Objects.equals(host, that.host)) { + return false; + } + if (!Objects.equals(port, that.port)) { + return false; + } + if (!Objects.equals(username, that.username)) { + return false; + } + return Objects.equals(password, that.password); + } + + @Override + public int hashCode() { + int result = host != null ? host.hashCode() : 0; + result = 31 * result + (port != null ? port.hashCode() : 0); + result = 31 * result + (username != null ? username.hashCode() : 0); + result = 31 * result + (password != null ? password.hashCode() : 0); + return result; + } + + @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..788ff11c17e 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,14 @@ public final class SocketSettings { private final long readTimeoutMS; private final int receiveBufferSize; private final int sendBufferSize; + /** + * NOTE: This setting is only applicable to the synchronous variant of MongoClient. + */ + private final ProxySettings proxySettings; /** * Gets a builder for an instance of {@code SocketSettings}. + * * @return the builder */ public static Builder builder() { @@ -63,6 +69,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 +89,7 @@ public Builder applySettings(final SocketSettings socketSettings) { readTimeoutMS = socketSettings.readTimeoutMS; receiveBufferSize = socketSettings.receiveBufferSize; sendBufferSize = socketSettings.sendBufferSize; + proxySettingsBuilder.applySettings(socketSettings.getProxySettings()); return this; } @@ -132,6 +140,21 @@ public Builder sendBufferSize(final int sendBufferSize) { return this; } + /** + * Applies the {@link ProxySettings.Builder} block and then sets the {@link SocketSettings#proxySettings}. + * + *

+ * NOTE: This setting is only applicable to the synchronous variant of MongoClient. + * + * @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 +174,8 @@ public Builder applyConnectionString(final ConnectionString connectionString) { this.readTimeout(socketTimeout, MILLISECONDS); } + proxySettingsBuilder.applyConnectionString(connectionString); + return this; } @@ -184,8 +209,18 @@ 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. + */ + public ProxySettings getProxySettings() { + return proxySettings; + } + /** * Gets the receive buffer size. Defaults to the operating system default. + * * @return the receive buffer size */ public int getReceiveBufferSize() { @@ -240,11 +275,11 @@ public int hashCode() { @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 +287,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/InetAddresses.java b/driver-core/src/main/com/mongodb/internal/connection/InetAddresses.java new file mode 100644 index 00000000000..c882b31d6d9 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/connection/InetAddresses.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 InetAddresses { + 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 InetAddresses() { + } + + /** + * 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..0993ad46267 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SocketStream.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SocketStream.java @@ -19,6 +19,7 @@ import com.mongodb.MongoSocketException; import com.mongodb.MongoSocketOpenException; import com.mongodb.MongoSocketReadException; +import com.mongodb.connection.ProxySettings; import com.mongodb.ServerAddress; import com.mongodb.connection.AsyncCompletionHandler; import com.mongodb.connection.BufferProvider; @@ -28,6 +29,8 @@ 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.getHost() != null) { + 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,32 @@ protected Socket initializeSocket() throws IOException { throw new MongoSocketException("Exception opening socket", getAddress()); } + private SSLSocket initializeSslSocketOverSocksProxy(final SSLSocketFactory sslSocketFactory) throws IOException { + String serverHost = address.getHost(); + int serverPort = address.getPort(); + + SocksSocket socksProxy = new SocksSocket(null, settings.getProxySettings()); + configureSocket(socksProxy, settings); + InetSocketAddress inetSocketAddress = InetSocketAddress.createUnresolved(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; + } + + private Socket initializeSocketOverSocksProxy() throws IOException { + Socket createdSocket = socketFactory.createSocket(); + SocksSocket socksProxy = new SocksSocket(createdSocket, settings.getProxySettings()); + configureSocket(createdSocket, settings); + + socksProxy.connect(InetSocketAddress.createUnresolved(address.getHost(), address.getPort()), + settings.getConnectTimeout(TimeUnit.MILLISECONDS)); + return createdSocket; + } + @Override public ByteBuf getBuffer(final int size) { return bufferProvider.getBuffer(size); @@ -170,6 +214,10 @@ SocketSettings getSettings() { return settings; } + SocketFactory getSocketFactory() { + return socketFactory; + } + @Override public void close() { try { 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..4eb2592d275 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java @@ -0,0 +1,444 @@ +/* + * 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.internal.VisibleForTesting; +import com.mongodb.lang.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +import static com.mongodb.assertions.Assertions.assertNotNull; +import static com.mongodb.assertions.Assertions.assertTrue; +import static com.mongodb.assertions.Assertions.fail; + +/** + *

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 Pattern DOMAIN_PATTERN = Pattern.compile("^(([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}|localhost)$"); + private static final byte LENGTH_OF_IPV4 = 4; + private static final byte LENGTH_OF_IPV6 = 16; + private static final byte SOCKS_VERSION = 5; + private static final byte RESERVED = 0x00; + private static final byte PORT_SIZE = 2; + private static final byte AUTHENTICATION_SUCCEEDED_STATUS = 0x00; + private static final byte REQUEST_OK = 0; + private static final byte ADDRESS_TYPE_DOMAIN_NAME = 3; + private static final byte ADDRESS_TYPE_IPV4 = 1; + private static final byte ADDRESS_TYPE_IPV6 = 4; + private static final int DEFAULT_PORT = 1080; + public static final String IP_PARSING_ERROR_SUFFIX = " is not an IP string literal"; + private final SocksAuthenticationMethod[] authenticationMethods; + private final InetSocketAddress proxyAddress; + private InetSocketAddress remoteAddress; + @Nullable + private String proxyUsername; + @Nullable + private String proxyPassword; + @Nullable + private final Socket socket; + private InputStream inputStream; + private OutputStream outputStream; + + public SocksSocket(final ProxySettings proxySettings) { + this(null, proxySettings); + } + + public SocksSocket(@Nullable final Socket socket, final ProxySettings proxySettings) { + int port = getPort(proxySettings); + assertTrue(proxySettings.getHost() != null); + assertTrue(port >= 0); + + this.proxyAddress = new InetSocketAddress(proxySettings.getHost(), port); + this.socket = socket; + + if (proxySettings.getUsername() != null && proxySettings.getPassword() != null) { + this.authenticationMethods = new SocksAuthenticationMethod[]{ + SocksAuthenticationMethod.NO_AUTH, + SocksAuthenticationMethod.USERNAME_PASSWORD}; + this.proxyUsername = proxySettings.getUsername(); + this.proxyPassword = proxySettings.getPassword(); + } else { + this.authenticationMethods = new SocksAuthenticationMethod[]{SocksAuthenticationMethod.NO_AUTH}; + } + } + + private static int getPort(final ProxySettings proxySettings) { + if (proxySettings.getPort() != null) { + return proxySettings.getPort(); + } + return DEFAULT_PORT; + } + + @Override + public void connect(final SocketAddress endpoint) throws IOException { + connect(endpoint, 0); + } + + @Override + public void connect(final SocketAddress endpoint, final int timeoutMs) throws IOException { + try { + Timeout timeout = toTimeout(timeoutMs); + InetSocketAddress unresolvedAddress = (InetSocketAddress) endpoint; + assertTrue(unresolvedAddress.isUnresolved()); + this.remoteAddress = unresolvedAddress; + if (socket != null && !socket.isConnected()) { + socket.connect(proxyAddress, remainingMillis(timeout)); + inputStream = socket.getInputStream(); + outputStream = socket.getOutputStream(); + } else { + super.connect(proxyAddress, remainingMillis(timeout)); + inputStream = getInputStream(); + outputStream = getOutputStream(); + } + SocksAuthenticationMethod authenticationMethod = performHandshake(timeout); + authenticate(authenticationMethod, timeout); + sendConnect(timeout); + } catch (SocketException socketException) { + 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.UTF_8); + final int hostLength = host.length(); + + byte addressType; + byte[] ipAddress = null; + if (isDomainName(host)) { + addressType = ADDRESS_TYPE_DOMAIN_NAME; + } else { + ipAddress = createByteArrayFromIpAddress(host); + addressType = determineAddressType(ipAddress); + } + byte[] bufferSent = createBuffer(addressType, hostLength); + bufferSent[0] = SOCKS_VERSION; + bufferSent[1] = (byte) SocksCommand.CONNECT.getCommandNumber(); + bufferSent[2] = RESERVED; + switch (addressType) { + case ADDRESS_TYPE_DOMAIN_NAME: + bufferSent[3] = ADDRESS_TYPE_DOMAIN_NAME; + bufferSent[4] = (byte) hostLength; + System.arraycopy(bytesOfHost, 0, bufferSent, 5, hostLength); + bufferSent[5 + host.length()] = (byte) ((port & 0xff00) >> 8); + bufferSent[6 + host.length()] = (byte) (port & 0xff); + break; + case ADDRESS_TYPE_IPV4: + bufferSent[3] = ADDRESS_TYPE_IPV4; + System.arraycopy(ipAddress, 0, bufferSent, 4, ipAddress.length); + bufferSent[4 + ipAddress.length] = (byte) ((port & 0xff00) >> 8); + bufferSent[5 + ipAddress.length] = (byte) (port & 0xff); + break; + case ADDRESS_TYPE_IPV6: + bufferSent[3] = ADDRESS_TYPE_IPV6; + System.arraycopy(ipAddress, 0, bufferSent, 4, ipAddress.length); + bufferSent[4 + ipAddress.length] = (byte) ((port & 0xff00) >> 8); + bufferSent[5 + ipAddress.length] = (byte) (port & 0xff); + break; + default: + fail(); + } + outputStream.write(bufferSent); + outputStream.flush(); + checkServerReply(timeout); + } + + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + public static boolean isDomainName(final String host) { + return DOMAIN_PATTERN.matcher(host).matches(); + } + + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + public static byte[] createByteArrayFromIpAddress(final String host) throws SocketException { + byte[] bytes = InetAddresses.ipStringToBytes(host); + if (bytes == null) { + throw new SocketException(host + IP_PARSING_ERROR_SUFFIX); + } + return bytes; + } + + private byte determineAddressType(final byte[] ipAddress) { + if (ipAddress.length == LENGTH_OF_IPV4) { + return ADDRESS_TYPE_IPV4; + } else if (ipAddress.length == LENGTH_OF_IPV6) { + return ADDRESS_TYPE_IPV6; + } + throw fail(); + } + + private static byte[] createBuffer(final byte addressType, final int hostLength) { + switch (addressType) { + case ADDRESS_TYPE_DOMAIN_NAME: + return new byte[7 + hostLength]; + case ADDRESS_TYPE_IPV4: + return new byte[6 + LENGTH_OF_IPV4]; + case ADDRESS_TYPE_IPV6: + return new byte[6 + LENGTH_OF_IPV6]; + default: + break; + } + throw fail(); + } + + private void checkServerReply(final Timeout timeout) throws IOException { + byte[] data = readSocksReply(inputStream, 4, timeout); + byte status = data[1]; + + if (status == REQUEST_OK) { + switch (data[3]) { + case ADDRESS_TYPE_IPV4: + readSocksReply(inputStream, 4 + PORT_SIZE, timeout); + break; + case ADDRESS_TYPE_DOMAIN_NAME: + byte hostNameLength = readSocksReply(inputStream, 1, timeout)[0]; + readSocksReply(inputStream, hostNameLength + PORT_SIZE, timeout); + break; + case ADDRESS_TYPE_IPV6: + readSocksReply(inputStream, 16 + PORT_SIZE, timeout); + break; + default: + throw new ConnectException("Reply from SOCKS proxy server contains wrong code"); + } + return; + } + + for (ServerErrorReply serverErrorReply : ServerErrorReply.values()) { + if (status == serverErrorReply.getReplyNumber()) { + throw new ConnectException(serverErrorReply.getMessage()); + } + } + throw new ConnectException("Unknown status"); + } + + private void authenticate(final SocksAuthenticationMethod authenticationMethod, final Timeout timeout) throws IOException { + if (authenticationMethod == SocksAuthenticationMethod.USERNAME_PASSWORD) { + final byte[] bytesOfUsername = assertNotNull(proxyUsername).getBytes(StandardCharsets.UTF_8); + final byte[] bytesOfPassword = assertNotNull(proxyPassword).getBytes(StandardCharsets.UTF_8); + final int usernameLength = bytesOfUsername.length; + final int passwordLength = bytesOfPassword.length; + final byte[] command = new byte[3 + usernameLength + passwordLength]; + + command[0] = 0x01; + 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.write(command); + outputStream.flush(); + + byte[] authenticationResult = readSocksReply(inputStream, 2, timeout); + + if (authenticationResult[1] != AUTHENTICATION_SUCCEEDED_STATUS) { + /* RFC 1929 specifies that the connection MUST be closed if + authentication fails */ + throw new ConnectException("Authentication failed"); + } + } + } + + private SocksAuthenticationMethod performHandshake(final Timeout timeout) throws IOException { + 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] = (byte) authenticationMethods[i].getMethodNumber(); + } + outputStream.write(bufferSent); + outputStream.flush(); + + byte[] handshakeReply = readSocksReply(inputStream, 2, timeout); + + if (handshakeReply[0] != SOCKS_VERSION) { + throw new ConnectException("Remote server doesn't support SOCKS5"); + } + if (handshakeReply[1] == (byte) 0xFF) { + throw new ConnectException("None of the authentication methods listed are acceptable"); + } + if (handshakeReply[1] == SocksAuthenticationMethod.NO_AUTH.getMethodNumber()) { + return SocksAuthenticationMethod.NO_AUTH; + } + + return SocksAuthenticationMethod.USERNAME_PASSWORD; + } + + 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 = (int) timeout.remaining(TimeUnit.MILLISECONDS); + if (remaining > 0) { + return remaining; + } + + throw new SocketTimeoutException("Socket connection timed out"); + } + + private byte[] readSocksReply(final InputStream in, final int length, final Timeout timeout) throws IOException { + byte[] data = new byte[length]; + int received = 0; + int originalTimeout = getSoTimeout(); + try { + while (received < length) { + int count; + int remaining = remainingMillis(timeout); + setSoTimeout(remaining); + try { + count = in.read(data, received, length - received); + } catch (SocketTimeoutException e) { + throw new SocketTimeoutException("Socket connection timed out"); + } + if (count < 0) { + throw new ConnectException("Malformed reply from SOCKS proxy server"); + } + received += count; + } + } finally { + setSoTimeout(originalTimeout); + } + return data; + } + + public static byte[] read(final InputStream inputStream, final int length) throws IOException { + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) { + int read = inputStream.read(); + if (read < 0) { + throw new ConnectException("End of stream"); + } + bytes[i] = (byte) read; + } + return bytes; + } + + @Override + public synchronized void close() throws IOException { + if (socket != null) { + socket.close(); + } else { + super.close(); + } + } + + @Override + public synchronized void setSoTimeout(final int timeout) throws SocketException { + if (socket != null) { + socket.setSoTimeout(timeout); + } else { + super.setSoTimeout(timeout); + } + } + + @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(); + } + + private enum SocksCommand { + + CONNECT(0x01); + + private final int value; + + SocksCommand(final int value) { + this.value = value; + } + + public int getCommandNumber() { + return value; + } + } + + private enum SocksAuthenticationMethod { + NO_AUTH(0x00), + USERNAME_PASSWORD(0x02); + + private final int methodNumber; + + SocksAuthenticationMethod(final int methodNumber) { + this.methodNumber = methodNumber; + } + + public int getMethodNumber() { + return methodNumber; + } + + } + + private enum ServerErrorReply { + GENERAL_FAILURE(1, "Remote server doesn't support SOCKS5"), + NOT_ALLOWED(2, "Proxy server general failure"), + NET_UNREACHABLE(3, "Connection not allowed by ruleset"), + HOST_UNREACHABLE(4, "Network is unreachable"), + CONN_REFUSED(5, "Host is unreachable"), + TTL_EXPIRED(6, "Connection has been refused"), + CMD_NOT_SUPPORTED(7, "TTL expired"), + ADDR_TYPE_NOT_SUP(8, "Address type not supported"); + + private final int replyNumber; + private final String message; + + ServerErrorReply(final int replyNumber, final String message) { + this.replyNumber = replyNumber; + this.message = message; + } + + public int getReplyNumber() { + return replyNumber; + } + + public String getMessage() { + return message; + } + } +} 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/SocksSocketTest.java b/driver-core/src/test/functional/com/mongodb/internal/SocksSocketTest.java new file mode 100644 index 00000000000..fad058fd422 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/internal/SocksSocketTest.java @@ -0,0 +1,191 @@ +/* + * 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; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.net.SocketException; + +import static com.mongodb.internal.connection.SocksSocket.createByteArrayFromIpAddress; +import static com.mongodb.internal.connection.SocksSocket.isDomainName; + +class SocksSocketTest { + private static final byte LENGTH_OF_IPV4 = 4; + private static final byte LENGTH_OF_IPV6 = 16; + private static final String IP_PARSING_ERROR_SUFFIX = " is not an IP string literal"; + + @ParameterizedTest + @ValueSource(strings = { + "2001:db8:85a3::8a2e:370:7334", + "::5000", + "5000::", + "1:2:3:4:5:6:7:8", + "0:0:0:0:0:0:0:2", + "1:2:3:4:5:6::7", + "::1:2:3:4:5:6:7", + "1:2:3:4:5:6:7::", + "::2", + "0:000::0:2", + "2001:db8:85a3::8a2e:370:7334", + "1::", + "0::1", + "::0:0000:0", + "::", + "::1", + "0:0:0:0:0:0:0:0", + "0:0:0:0:0:0:0:1", + }) + void shouldReturnIpv6Address(final String ipAddress) throws SocketException { + Assertions.assertEquals(LENGTH_OF_IPV6, createByteArrayFromIpAddress(ipAddress).length); + } + + + @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", + }) + 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", + "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" + }) + void shouldReturnFalseWithInvalidHostName(final String hostname) { + Assertions.assertFalse(isDomainName(hostname)); + } + + @ParameterizedTest + @ValueSource(strings = { + "192.168.0.1", + "10.0.0.1", + "172.16.0.1", + "255.255.255.255", + "127.0.0.1", + "169.254.1.2", + "1.2.3.4" + }) + void shouldReturnIpv4Address(final String ipAddress) throws SocketException { + Assertions.assertEquals(LENGTH_OF_IPV4, createByteArrayFromIpAddress(ipAddress).length); + } + + @ParameterizedTest + @ValueSource(strings = { + //Invalid IPV4 addresses + "256.0.0.1", + "192.168.256.1", + "192.168.0.", + "300.300.300.300", + "192.168.0.0.1", + "110.010.20.030", // octal representation + "008.8.8.8", // octal representation + "007.008.009.010", // octal representation + + //Invalid IPV6 addresses + "::::", + "0:1:2:3:4:5:6::7", + "0::1:2:3:4:5:6:7", + "0:1:2::3:4:5:6:7", + "::1:2:3:4:5:6:7:8", + "1:2:3:4:5:6:7:8::", + "0:1:2:3:4:5:6:7:8:9", + "::5000::", + "5::3::4", + "::5::4::", + "::5::4", + "4::5::", + "1::2:3::4", + "1:2", + "2:::5", + "1::2::5", + ":4:", + ":7", + "7:", + "1", + ":5:2", + "5:2:", + "1:2:3:4:5:6:7", + ":::::", + ":::", + "::n::", + "2001:db8:85a3::8a2e:370:7334:", + ":2001:db8:85a3::8a2e:370:7334", + "20012:db8:85a3::8a2e:370:7334", + "20012:20012:20012:20012:20012:20012:20012:20012", + + //Domain names + "localhost", + "3letter.xyz", + "my_domain.com", + "hyphenated-name.net", + "numbers123.org", + "_underscored.site", + "xn--80ak6aa92e.com (IDN)", + "localhost", + "www.ab--cd.com", + ".invalid", + "example.invalid", + "test.-site.com", + "subdomain..domain.com", + "256charactersinthisdomainnamethatexceedsthemaximumallowedlengthfortld.com", + ".xn--32-6kcakeb6cn8ak4b1d4dkswnqn.xn--pss-3p4d1dm5a.xn--jlq61u9w3b (Punycode)", + "--doublehyphens.org", + "subdomain.toolongtldddddddddddddd", + "spaced out.site", + "no_spaces.domain.com", + "my hostname.com", + "localhost:8080", + "www.example.com.", + "-startingwithhyphen.net", + "this-domain-is-really-long-because-it-just-keeps-going-and-going-and-its-still-not-done-yet-because-theres-more.net", + "sub--sub.domain.com", + "mydomain.", + "www..com", + "subdomain.no_underscores_or_dots_allowed,", + }) + void shouldThrowErrorWhenInvalidIpAddressIsProvided(final String ipAddress) { + SocketException socketException = Assertions.assertThrows(SocketException.class, () -> createByteArrayFromIpAddress(ipAddress)); + Assertions.assertEquals(ipAddress + IP_PARSING_ERROR_SUFFIX, socketException.getMessage()); + } + +} diff --git a/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy b/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy index 63cd8aeb27a..633fecd0432 100644 --- a/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy @@ -377,6 +377,29 @@ 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 equal or greater than 0' | 'mongodb://localhost:27017/?proxyHost=a&proxyPort=-1' + '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=' + 'Both proxyUsername' + + ' and proxyPassword must be set together.' + + ' They cannot be set individually' | 'mongodb://localhost:27017/?proxyHost=a&proxyPassword=1' + } + @Unroll def 'should throw IllegalArgumentException when the string #cause'() { when: 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..b868b9ab9fc --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/ProxySettingsTest.java @@ -0,0 +1,82 @@ +/* + * 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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +class ProxySettingsTest { + static Stream shouldThrowExceptionWhenProxySettingAreInInvalid() { + return Stream.of( + Arguments.of(ProxySettings.builder() + .port(1080), "state should be: proxyPort can only be specified with proxyHost"), + Arguments.of(ProxySettings.builder() + .port(1080) + .username("test") + .password("test"), "state should be: proxyPort can only be specified with proxyHost"), + Arguments.of(ProxySettings.builder() + .username("test"), "state should be: proxyUsername can only be specified with proxyHost"), + Arguments.of(ProxySettings.builder() + .password("test"), "state should be: proxyPassword can only be specified with proxyHost"), + Arguments.of(ProxySettings.builder() + .host("test") + .username("test"), + "state should be: Both proxyUsername and proxyPassword must be set together. They cannot be set individually"), + Arguments.of(ProxySettings.builder() + .host("test") + .password("test"), + "state should be: Both proxyUsername and proxyPassword must be set together. They cannot be set individually") + ); + } + + @ParameterizedTest + @MethodSource + void shouldThrowExceptionWhenProxySettingAreInInvalid(final ProxySettings.Builder builder, final String expectedErrorMessage) { + IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, builder::build); + Assertions.assertEquals(expectedErrorMessage, exception.getMessage()); + } + + static Stream shouldNotThrowExceptionWhenProxySettingAreValid() { + return Stream.of( + Arguments.of(ProxySettings.builder() + .host("test") + .port(1080)), + Arguments.of(ProxySettings.builder() + .host("test")), + Arguments.of(ProxySettings.builder() + .host("test") + .port(1080) + .username("test") + .password("test")), + Arguments.of(ProxySettings.builder() + .host("test") + .username("test") + .password("test")) + ); + } + + @ParameterizedTest + @MethodSource + void shouldNotThrowExceptionWhenProxySettingAreValid(final ProxySettings.Builder builder) { + builder.build(); + } +} 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..193d0b657be 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; @@ -110,6 +111,9 @@ public static MongoClient create(final MongoClientSettings settings) { */ public static MongoClient create(final MongoClientSettings settings, @Nullable final MongoDriverInformation mongoDriverInformation) { if (settings.getStreamFactoryFactory() == null) { + if (settings.getSocketSettings().getProxySettings().getHost() != null){ + throw new MongoClientException("Proxy is not supported for reactive clients"); + } if (settings.getSslSettings().isEnabled()) { return createWithTlsChannel(settings, mongoDriverInformation); } else { 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..745714e264e --- /dev/null +++ b/driver-scala/src/main/scala/org/mongodb/scala/connection/ProxySettings.scala @@ -0,0 +1,41 @@ +/* + * 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 } + +/** + * An immutable class representing settings for connecting to MongoDB via a SOCKS5 proxy server. + * NOTE: This setting is only applicable to the synchronous variant of MongoClient and Key Management Service settings. + * + * @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..17d44ca5657 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,12 @@ package object connection { */ type SocketSettings = com.mongodb.connection.SocketSettings + /** + * Settings for connecting to MongoDB via proxy server. + * NOTE: This setting is only applicable to the synchronous variant of MongoClient and Key Management Service settings. + */ + 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/main/com/mongodb/client/internal/Crypts.java b/driver-sync/src/main/com/mongodb/client/internal/Crypts.java index 73e4d42e8ef..bd5fb51a45f 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/Crypts.java +++ b/driver-sync/src/main/com/mongodb/client/internal/Crypts.java @@ -20,10 +20,12 @@ import com.mongodb.ClientEncryptionSettings; import com.mongodb.MongoClientSettings; import com.mongodb.MongoNamespace; +import com.mongodb.connection.ProxySettings; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.crypt.capi.MongoCrypt; import com.mongodb.crypt.capi.MongoCrypts; +import com.mongodb.lang.Nullable; import javax.net.ssl.SSLContext; import java.util.Map; @@ -48,10 +50,14 @@ public static Crypt createCrypt(final MongoClientImpl client, final AutoEncrypti MongoClient keyVaultClient = keyVaultMongoClientSettings == null ? sharedInternalClient : MongoClients.create(keyVaultMongoClientSettings); MongoCrypt mongoCrypt = MongoCrypts.create(createMongoCryptOptions(settings)); + + ProxySettings kmsProxySettings = settings.getProxySettings() == null ? client.getSettings() + .getSocketSettings().getProxySettings() : settings.getProxySettings(); + return new Crypt( mongoCrypt, createKeyRetriever(keyVaultClient, settings.getKeyVaultNamespace()), - createKeyManagementService(settings.getKmsProviderSslContextMap()), + createKeyManagementService(settings.getKmsProviderSslContextMap(), kmsProxySettings), settings.getKmsProviders(), settings.getKmsProviderPropertySuppliers(), settings.isBypassAutoEncryption(), @@ -63,7 +69,7 @@ public static Crypt createCrypt(final MongoClientImpl client, final AutoEncrypti static Crypt create(final MongoClient keyVaultClient, final ClientEncryptionSettings settings) { return new Crypt(MongoCrypts.create(createMongoCryptOptions(settings)), createKeyRetriever(keyVaultClient, settings.getKeyVaultNamespace()), - createKeyManagementService(settings.getKmsProviderSslContextMap()), + createKeyManagementService(settings.getKmsProviderSslContextMap(), settings.getProxySettings()), settings.getKmsProviders(), settings.getKmsProviderPropertySuppliers() ); @@ -73,8 +79,9 @@ private static KeyRetriever createKeyRetriever(final MongoClient keyVaultClient, return new KeyRetriever(keyVaultClient, new MongoNamespace(keyVaultNamespaceString)); } - private static KeyManagementService createKeyManagementService(final Map kmsProviderSslContextMap) { - return new KeyManagementService(kmsProviderSslContextMap, 10000); + private static KeyManagementService createKeyManagementService(final Map kmsProviderSslContextMap, + @Nullable final ProxySettings proxySettings) { + return new KeyManagementService(kmsProviderSslContextMap, proxySettings, 10000); } private Crypts() { diff --git a/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java b/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java index c6c04eec0c3..3b2f7417987 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java +++ b/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java @@ -16,12 +16,14 @@ package com.mongodb.client.internal; +import com.mongodb.connection.ProxySettings; import com.mongodb.ServerAddress; +import com.mongodb.internal.connection.SocksSocket; +import com.mongodb.internal.connection.SslHelper; import com.mongodb.internal.diagnostics.logging.Logger; import com.mongodb.internal.diagnostics.logging.Loggers; -import com.mongodb.internal.connection.SslHelper; +import com.mongodb.lang.Nullable; -import javax.net.SocketFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocket; @@ -35,16 +37,21 @@ import java.nio.ByteBuffer; import java.util.Map; +import static com.mongodb.assertions.Assertions.assertNotNull; import static com.mongodb.assertions.Assertions.notNull; class KeyManagementService { private static final Logger LOGGER = Loggers.getLogger("client"); private final Map kmsProviderSslContextMap; private final int timeoutMillis; + @Nullable + private final ProxySettings proxySettings; - KeyManagementService(final Map kmsProviderSslContextMap, final int timeoutMillis) { + KeyManagementService(final Map kmsProviderSslContextMap, @Nullable final ProxySettings proxySettings, + final int timeoutMillis) { this.kmsProviderSslContextMap = notNull("kmsProviderSslContextMap", kmsProviderSslContextMap); this.timeoutMillis = timeoutMillis; + this.proxySettings = proxySettings; } public InputStream stream(final String kmsProvider, final String host, final ByteBuffer message) throws IOException { @@ -52,17 +59,26 @@ public InputStream stream(final String kmsProvider, final String host, final Byt LOGGER.info("Connecting to KMS server at " + serverAddress); SSLContext sslContext = kmsProviderSslContextMap.get(kmsProvider); + SSLSocketFactory sslSocketFactory = sslContext == null + ? (SSLSocketFactory) SSLSocketFactory.getDefault() : sslContext.getSocketFactory(); - SocketFactory sslSocketFactory = sslContext == null - ? SSLSocketFactory.getDefault() : sslContext.getSocketFactory(); - SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(); - enableHostNameVerification(socket); - + Socket socket = null; try { - socket.setSoTimeout(timeoutMillis); - socket.connect(new InetSocketAddress(InetAddress.getByName(serverAddress.getHost()), serverAddress.getPort()), timeoutMillis); + if (isProxyEnabled()) { + socket = new SocksSocket(assertNotNull(proxySettings)); + socket.setSoTimeout(timeoutMillis); + socket.connect(InetSocketAddress.createUnresolved(host, serverAddress.getPort()), timeoutMillis); + socket = sslSocketFactory.createSocket(socket, host, serverAddress.getPort(), true); + } else { + socket = sslSocketFactory.createSocket(); + socket.connect(new InetSocketAddress(InetAddress.getByName(serverAddress.getHost()), serverAddress.getPort()), + timeoutMillis); + } + enableHostNameVerification((SSLSocket) socket); } catch (IOException e) { - closeSocket(socket); + if (socket != null) { + closeSocket(socket); + } throw e; } @@ -86,6 +102,10 @@ public InputStream stream(final String kmsProvider, final String host, final Byt } } + private boolean isProxyEnabled() { + return proxySettings != null && proxySettings.getHost() != null; + } + private void enableHostNameVerification(final SSLSocket socket) { SSLParameters sslParameters = socket.getSSLParameters(); if (sslParameters == null) { 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..d1fd908289a --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java @@ -0,0 +1,234 @@ +/* + * 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.ClusterConnectionMode; +import com.mongodb.connection.ClusterDescription; +import com.mongodb.connection.ServerDescription; +import com.mongodb.event.ClusterDescriptionChangedEvent; +import com.mongodb.event.ClusterListener; +import com.mongodb.lang.Nullable; +import org.bson.Document; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +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.StringJoiner; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +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 Boolean SOCKS_AUTH_ENABLED = Boolean.valueOf(System.getProperty("org.mongodb.test.uri.socks.auth.enabled")); + private static final String PROXY_HOST = System.getProperty("org.mongodb.test.uri.proxyHost"); + 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 noAuthSettings() { + return Stream.of(buildConnectionString(MONGO_SINGLE_MAPPED_URI_PREFIX, true), + buildConnectionString(MONGO_REPLICA_SET_URI_PREFIX, false)); + } + + static Stream invalidAuthSettings() { + return Stream.of(buildConnectionString(MONGO_SINGLE_MAPPED_URI_PREFIX, true, "nonexistentuser", "badauth"), + buildConnectionString(MONGO_REPLICA_SET_URI_PREFIX, false, "nonexistentuser", "badauth")); + } + + static Stream validAuthSettings() { + return Stream.of(buildConnectionString(MONGO_SINGLE_MAPPED_URI_PREFIX, true, "username", "p4ssw0rd"), + buildConnectionString(MONGO_REPLICA_SET_URI_PREFIX, false, "username", "p4ssw0rd")); + } + + @ParameterizedTest(name = "Should connect without authentication in connection string. ConnectionString: {0}") + @MethodSource({"noAuthSettings", "invalidAuthSettings"}) + void shouldConnectWithoutAuth(final ConnectionString connectionString) { + assumeFalse(SOCKS_AUTH_ENABLED); + mongoClient = MongoClients.create(connectionString); + runHelloCommand(mongoClient); + } + + @ParameterizedTest(name = "Should connect without authentication in proxy settings. ConnectionString: {0}") + @MethodSource({"noAuthSettings", "invalidAuthSettings"}) + void shouldConnectWithoutAuthInProxySettings(final ConnectionString connectionString) { + assumeFalse(SOCKS_AUTH_ENABLED); + mongoClient = MongoClients.create(buildMongoClientSettings(connectionString)); + runHelloCommand(mongoClient); + } + + @ParameterizedTest(name = "Should not connect without valid authentication in connection string. ConnectionString: {0}") + @MethodSource({"noAuthSettings", "invalidAuthSettings"}) + void shouldNotConnectWithoutAuth(final ConnectionString connectionString) { + assumeTrue(SOCKS_AUTH_ENABLED); + ClusterListener clusterListener = Mockito.mock(ClusterListener.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(ClusterDescriptionChangedEvent.class); + + mongoClient = createMongoClient(MongoClientSettings.builder() + .applyConnectionString(connectionString), clusterListener); + + Assertions.assertThrows(MongoTimeoutException.class, () -> runHelloCommand(mongoClient)); + assertSocksAuthenticationIssue(clusterListener, captor); + } + + @ParameterizedTest(name = "Should not connect without valid authentication in proxy settings. ConnectionString: {0}") + @MethodSource({"noAuthSettings", "invalidAuthSettings"}) + public void shouldNotConnectWithoutAuthInProxySettings(final ConnectionString connectionString) { + assumeTrue(SOCKS_AUTH_ENABLED); + ClusterListener clusterListener = Mockito.mock(ClusterListener.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(ClusterDescriptionChangedEvent.class); + + mongoClient = createMongoClient(MongoClientSettings.builder(buildMongoClientSettings(connectionString)), clusterListener); + + Assertions.assertThrows(MongoTimeoutException.class, () -> runHelloCommand(mongoClient)); + assertSocksAuthenticationIssue(clusterListener, captor); + } + + @ParameterizedTest(name = "Should connect with valid authentication in connection string. ConnectionString: {0}") + @MethodSource("validAuthSettings") + void shouldConnectWithValidAuth(final ConnectionString connectionString) { + assumeTrue(SOCKS_AUTH_ENABLED); + mongoClient = MongoClients.create(connectionString); + runHelloCommand(mongoClient); + } + + @ParameterizedTest(name = "Should connect with valid authentication in proxy settings. ConnectionString: {0}") + @MethodSource("validAuthSettings") + public void shouldConnectWithValidAuthInProxySettings(final ConnectionString connectionString) { + assumeTrue(SOCKS_AUTH_ENABLED); + mongoClient = MongoClients.create(buildMongoClientSettings(connectionString)); + runHelloCommand(mongoClient); + } + + private static void assertSocksAuthenticationIssue(final ClusterListener clusterListener, + final ArgumentCaptor captor) { + 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 boolean directConnection, + @Nullable final String proxyUsername, + @Nullable final String proxyPassword) { + StringJoiner joiner; + if (uriPrefix.contains("/?")) { + joiner = new StringJoiner("&", "&", ""); + } else { + joiner = new StringJoiner("&", "/?", ""); + } + + joiner.add("proxyHost=" + PROXY_HOST) + .add("proxyPort=" + PROXY_PORT); + if (proxyPassword != null && proxyUsername != null) { + joiner.add("proxyPassword=" + proxyPassword) + .add("proxyUsername=" + proxyUsername); + } + if (directConnection) { + joiner.add("directConnection=" + true); + } + return new ConnectionString(uriPrefix + joiner); + } + + private static ConnectionString buildConnectionString(final String uriPrefix, final boolean directConnection) { + return buildConnectionString(uriPrefix, directConnection, null, null); + } + + private static MongoClientSettings buildMongoClientSettings(final ConnectionString connectionString) { + return MongoClientSettings.builder().applyToSocketSettings(builder -> builder.applyToProxySettings(proxyBuilder -> { + proxyBuilder.host(connectionString.getProxyHost()); + proxyBuilder.port(connectionString.getProxyPort()); + if (connectionString.getProxyUsername() != null) { + proxyBuilder.username(connectionString.getProxyUsername()); + } + if (connectionString.getProxyPassword() != null) { + proxyBuilder.password(connectionString.getProxyPassword()); + } + })).applyToClusterSettings(builder -> { + if (connectionString.isDirectConnection() != null && connectionString.isDirectConnection()) { + builder.mode(ClusterConnectionMode.SINGLE); + } + }).applyToSslSettings(sslBuilder -> { + if (connectionString.getSslEnabled() != null && connectionString.getSslEnabled()) { + sslBuilder.enabled(connectionString.getSslEnabled()); + } + if (connectionString.getSslInvalidHostnameAllowed() != null && connectionString.getSslInvalidHostnameAllowed()) { + sslBuilder.invalidHostNameAllowed(connectionString.getSslInvalidHostnameAllowed()); + } + }) + .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()); + } + + 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 enabled because socks proxy configuration exists"); + } else { + return ConditionEvaluationResult.disabled("Test disabled because socks proxy configuration is missing"); + } + } + } +} From 07a567d01dc5535d0a876cb109f9e96e76483faf Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 16 Aug 2023 18:34:06 -0700 Subject: [PATCH 02/27] Include CSFLE test suites to run via Socks5 proxy. JAVA-4347 --- .evergreen/.evg.yml | 15 +++++++ .evergreen/run-socks5-tests.sh | 40 ++++++++++++++----- .../client/internal/KeyManagementService.java | 12 ++++-- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index 154d7bf5c8b..e888fe1210c 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -735,6 +735,19 @@ functions: working_dir: src script: | ${PREPARE_SHELL} + export AWS_ACCESS_KEY_ID=${aws_access_key_id} + export AWS_SECRET_ACCESS_KEY=${aws_secret_access_key} + export AWS_DEFAULT_REGION=us-east-1 + . ${DRIVERS_TOOLS}/.evergreen/csfle/set-temp-creds.sh + MONGODB_URI="${MONGODB_URI}" \ + AWS_ACCESS_KEY_ID=${aws_access_key_id} AWS_SECRET_ACCESS_KEY=${aws_secret_access_key} \ + AWS_TEMP_ACCESS_KEY_ID=$CSFLE_AWS_TEMP_ACCESS_KEY_ID \ + AWS_TEMP_SECRET_ACCESS_KEY=$CSFLE_AWS_TEMP_SECRET_ACCESS_KEY \ + AWS_TEMP_SESSION_TOKEN=$CSFLE_AWS_TEMP_SESSION_TOKEN \ + AZURE_TENANT_ID=${azure_tenant_id} AZURE_CLIENT_ID=${azure_client_id} AZURE_CLIENT_SECRET=${azure_client_secret} \ + GCP_EMAIL=${gcp_email} GCP_PRIVATE_KEY=${gcp_private_key} \ + AZUREKMS_KEY_VAULT_ENDPOINT=${testazurekms_keyvaultendpoint} \ + AZUREKMS_KEY_NAME=${testazurekms_keyname} \ SSL="${SSL}" MONGODB_URI="${MONGODB_URI}" \ JAVA_VERSION="${JAVA_VERSION}" \ .evergreen/run-socks5-tests.sh @@ -1619,6 +1632,7 @@ tasks: - name: test-socks5 tags: [] commands: + - func: start-kms-kmip-server - func: bootstrap mongo-orchestration vars: VERSION: latest @@ -1627,6 +1641,7 @@ tasks: - name: test-socks5-tls tags: [] commands: + - func: start-kms-kmip-server - func: bootstrap mongo-orchestration vars: VERSION: latest diff --git a/.evergreen/run-socks5-tests.sh b/.evergreen/run-socks5-tests.sh index 92e1b4a169c..eb2a0533b5e 100644 --- a/.evergreen/run-socks5-tests.sh +++ b/.evergreen/run-socks5-tests.sh @@ -43,18 +43,38 @@ provision_ssl () { export GRADLE_SSL_VARS="-Pssl.enabled=true -Pssl.keyStoreType=pkcs12 -Pssl.keyStore=`pwd`/client.pkc -Pssl.keyStorePassword=bithere -Pssl.trustStoreType=jks -Pssl.trustStore=`pwd`/mongo-truststore -Pssl.trustStorePassword=changeit" } +run_csfle_tests () { + local MONGODB_URI=$1 # Get MongoDB URI from the first argument + echo "Running CSFE tests with Java ${JAVA_VERSION} for $TOPOLOGY and connecting to $MONGODB_URI with socks5" + # By not specifying the path to the `crypt_shared` via the `org.mongodb.test.crypt.shared.lib.path` Java system property, + # we force the driver to start `mongocryptd` instead of loading and using `crypt_shared`. + ./gradlew -PjavaVersion=${JAVA_VERSION} -Dorg.mongodb.test.uri=${MONGODB_URI} \ + -Dorg.mongodb.test.fle.on.demand.credential.test.failure.enabled="true" \ + -Dorg.mongodb.test.fle.on.demand.credential.test.azure.keyVaultEndpoint="${AZUREKMS_KEY_VAULT_ENDPOINT}" \ + -Dorg.mongodb.test.fle.on.demand.credential.test.azure.keyName="${AZUREKMS_KEY_NAME}" \ + -Dorg.mongodb.test.awsAccessKeyId=${AWS_ACCESS_KEY_ID} -Dorg.mongodb.test.awsSecretAccessKey=${AWS_SECRET_ACCESS_KEY} \ + -Dorg.mongodb.test.tmpAwsAccessKeyId=${AWS_TEMP_ACCESS_KEY_ID} -Dorg.mongodb.test.tmpAwsSecretAccessKey=${AWS_TEMP_SECRET_ACCESS_KEY} -Dorg.mongodb.test.tmpAwsSessionToken=${AWS_TEMP_SESSION_TOKEN} \ + -Dorg.mongodb.test.azureTenantId=${AZURE_TENANT_ID} -Dorg.mongodb.test.azureClientId=${AZURE_CLIENT_ID} -Dorg.mongodb.test.azureClientSecret=${AZURE_CLIENT_SECRET} \ + -Dorg.mongodb.test.gcpEmail=${GCP_EMAIL} -Dorg.mongodb.test.gcpPrivateKey=${GCP_PRIVATE_KEY} \ + ${GRADLE_SSL_VARS} \ + --stacktrace --info --continue \ + driver-sync:test \ + --tests "*Client*Encryption*" +} + run_socks5_prose_tests () { -local proxyPort=$1 -local authEnabled=$2 +local PROXY_PORT=$1 +local AUTH_ENABLED=$2 + echo "Running Socks5 tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connecting to $MONGODB_URI with socks auth enabled $AUTH_ENABLED" ./gradlew -PjavaVersion=${JAVA_VERSION} -Dorg.mongodb.test.uri=${MONGODB_URI} \ -Dorg.mongodb.test.uri.singleHost=${MONGODB_URI_SINGLEHOST} \ -Dorg.mongodb.test.uri.proxyHost="127.0.0.1" \ - -Dorg.mongodb.test.uri.proxyPort=${proxyPort} \ - -Dorg.mongodb.test.uri.socks.auth.enabled=${authEnabled} \ + -Dorg.mongodb.test.uri.proxyPort=${PROXY_PORT} \ + -Dorg.mongodb.test.uri.socks.auth.enabled=${AUTH_ENABLED} \ ${GRADLE_SSL_VARS} \ --stacktrace --info --continue \ driver-sync:test \ - --tests "*.Socks5ProseTest*" + --tests "com.mongodb.client.Socks5ProseTest*" } ############################################ @@ -62,9 +82,7 @@ local authEnabled=$2 ############################################ # Set up keystore/truststore -if [ "${SSL}" = "ssl" ]; then - provision_ssl -fi +provision_ssl # First, test with Socks5 + authentication required echo "Running tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connecting to $MONGODB_URI with socks5 auth enabled" @@ -72,7 +90,9 @@ echo "Running tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connec "$PYTHON_BINARY" "$SOCKS5_SERVER_SCRIPT" --port 1080 --auth username:p4ssw0rd --map "127.0.0.1:12345 to $FIRST_HOST" & SOCKS5_SERVER_PID_1=$! trap "kill $SOCKS5_SERVER_PID_1" EXIT + run_socks5_prose_tests "1080" "true" +run_csfle_tests "$MONGODB_URI&proxyHost=127.0.0.1&proxyPort=1080&proxyUsername=username&proxyPassword=p4ssw0rd" # Second, test with Socks5 + no authentication echo "Running tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connecting to $MONGODB_URI with socks5 auth disabled" @@ -81,4 +101,6 @@ echo "Running tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connec # Set up trap to kill both processes when the script exits SOCKS5_SERVER_PID_2=$! trap "kill $SOCKS5_SERVER_PID_1; kill $SOCKS5_SERVER_PID_2" EXIT -run_socks5_prose_tests "1081" "false" \ No newline at end of file + +run_socks5_prose_tests "1081" "false" +run_csfle_tests "$MONGODB_URI&proxyHost=127.0.0.1&proxyPort=1081" \ No newline at end of file diff --git a/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java b/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java index 3b2f7417987..441cd5ef27d 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java +++ b/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java @@ -16,8 +16,8 @@ package com.mongodb.client.internal; -import com.mongodb.connection.ProxySettings; import com.mongodb.ServerAddress; +import com.mongodb.connection.ProxySettings; import com.mongodb.internal.connection.SocksSocket; import com.mongodb.internal.connection.SslHelper; import com.mongodb.internal.diagnostics.logging.Logger; @@ -34,6 +34,7 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; +import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.Map; @@ -64,14 +65,17 @@ public InputStream stream(final String kmsProvider, final String host, final Byt Socket socket = null; try { + final String serverHost = serverAddress.getHost(); + final int serverPort = serverAddress.getPort(); + if (isProxyEnabled()) { socket = new SocksSocket(assertNotNull(proxySettings)); socket.setSoTimeout(timeoutMillis); - socket.connect(InetSocketAddress.createUnresolved(host, serverAddress.getPort()), timeoutMillis); - socket = sslSocketFactory.createSocket(socket, host, serverAddress.getPort(), true); + socket.connect(InetSocketAddress.createUnresolved(serverHost, serverPort), timeoutMillis); + socket = sslSocketFactory.createSocket(socket, serverHost, serverPort, true); } else { socket = sslSocketFactory.createSocket(); - socket.connect(new InetSocketAddress(InetAddress.getByName(serverAddress.getHost()), serverAddress.getPort()), + socket.connect(new InetSocketAddress(InetAddress.getByName(serverHost), serverPort), timeoutMillis); } enableHostNameVerification((SSLSocket) socket); From 0231ae6182dd96c223f4da7c8688c26700146450 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Thu, 17 Aug 2023 08:14:39 -0700 Subject: [PATCH 03/27] Remove extra MONGODB_URI variable. JAVA-4347 --- .evergreen/.evg.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index e888fe1210c..2e778c7562e 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -739,7 +739,6 @@ functions: export AWS_SECRET_ACCESS_KEY=${aws_secret_access_key} export AWS_DEFAULT_REGION=us-east-1 . ${DRIVERS_TOOLS}/.evergreen/csfle/set-temp-creds.sh - MONGODB_URI="${MONGODB_URI}" \ AWS_ACCESS_KEY_ID=${aws_access_key_id} AWS_SECRET_ACCESS_KEY=${aws_secret_access_key} \ AWS_TEMP_ACCESS_KEY_ID=$CSFLE_AWS_TEMP_ACCESS_KEY_ID \ AWS_TEMP_SECRET_ACCESS_KEY=$CSFLE_AWS_TEMP_SECRET_ACCESS_KEY \ From 53c7973db4a58a06851088e1b0ab840a722256a0 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Thu, 17 Aug 2023 13:39:41 -0700 Subject: [PATCH 04/27] Remove unused imports. JAVA-4347 --- .../main/com/mongodb/client/internal/KeyManagementService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java b/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java index 441cd5ef27d..bea86c868d3 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java +++ b/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java @@ -34,7 +34,6 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.Map; From 81020fb23f970cc19d63f549e9dce6d934dcff2b Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Thu, 24 Aug 2023 12:03:47 -0700 Subject: [PATCH 05/27] Update driver-core/src/main/com/mongodb/connection/SocketSettings.java Co-authored-by: Valentin Kovalenko --- driver-core/src/main/com/mongodb/connection/SocketSettings.java | 1 + 1 file changed, 1 insertion(+) diff --git a/driver-core/src/main/com/mongodb/connection/SocketSettings.java b/driver-core/src/main/com/mongodb/connection/SocketSettings.java index 788ff11c17e..764df35baf2 100644 --- a/driver-core/src/main/com/mongodb/connection/SocketSettings.java +++ b/driver-core/src/main/com/mongodb/connection/SocketSettings.java @@ -213,6 +213,7 @@ public int getReadTimeout(final TimeUnit timeUnit) { * 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) */ public ProxySettings getProxySettings() { return proxySettings; From 1313753b5ccf7ed9b03df4e0732224ac20104604 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Thu, 24 Aug 2023 12:56:46 -0700 Subject: [PATCH 06/27] Update driver-core/src/main/com/mongodb/ClientEncryptionSettings.java Co-authored-by: Valentin Kovalenko --- driver-core/src/main/com/mongodb/ClientEncryptionSettings.java | 1 + 1 file changed, 1 insertion(+) diff --git a/driver-core/src/main/com/mongodb/ClientEncryptionSettings.java b/driver-core/src/main/com/mongodb/ClientEncryptionSettings.java index 0d5f46967a3..6de131308ad 100644 --- a/driver-core/src/main/com/mongodb/ClientEncryptionSettings.java +++ b/driver-core/src/main/com/mongodb/ClientEncryptionSettings.java @@ -181,6 +181,7 @@ public MongoClientSettings getKeyVaultMongoClientSettings() { * Gets the proxy settings used for connecting to Key Management Service via a SOCKS5 proxy server. * * @return The {@link ProxySettings} instance containing the SOCKS5 proxy configuration. + * @see Builder#proxySettings(ProxySettings) * @since 4.11 */ @Nullable From 37e087474fcd89822c553fd56d5b9b60b8ffa1da Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Thu, 24 Aug 2023 13:04:21 -0700 Subject: [PATCH 07/27] Update driver-core/src/main/com/mongodb/AutoEncryptionSettings.java Co-authored-by: Valentin Kovalenko --- driver-core/src/main/com/mongodb/AutoEncryptionSettings.java | 1 + 1 file changed, 1 insertion(+) diff --git a/driver-core/src/main/com/mongodb/AutoEncryptionSettings.java b/driver-core/src/main/com/mongodb/AutoEncryptionSettings.java index 9c570dc0b4b..00d77f7fb28 100644 --- a/driver-core/src/main/com/mongodb/AutoEncryptionSettings.java +++ b/driver-core/src/main/com/mongodb/AutoEncryptionSettings.java @@ -311,6 +311,7 @@ public MongoClientSettings getKeyVaultMongoClientSettings() { * Gets the proxy settings used for connecting to Key Management Service via a SOCKS5 proxy server. * * @return The {@link ProxySettings} instance containing the SOCKS5 proxy configuration. + * @see Builder#proxySettings(ProxySettings) * @since 4.11 */ @Nullable From 3790094080ed8d34df5199bf997c7710857d1525 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Fri, 25 Aug 2023 11:45:47 -0700 Subject: [PATCH 08/27] Update driver-core/src/main/com/mongodb/connection/ProxySettings.java Co-authored-by: Valentin Kovalenko --- .../com/mongodb/connection/ProxySettings.java | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/driver-core/src/main/com/mongodb/connection/ProxySettings.java b/driver-core/src/main/com/mongodb/connection/ProxySettings.java index 83257c7e065..e9c9891c5c7 100644 --- a/driver-core/src/main/com/mongodb/connection/ProxySettings.java +++ b/driver-core/src/main/com/mongodb/connection/ProxySettings.java @@ -185,31 +185,19 @@ public boolean equals(final Object o) { if (this == o) { return true; } - if (!(o instanceof ProxySettings)) { + if (o == null || getClass() != o.getClass()) { return false; } - - ProxySettings that = (ProxySettings) o; - - if (!Objects.equals(host, that.host)) { - return false; - } - if (!Objects.equals(port, that.port)) { - return false; - } - if (!Objects.equals(username, that.username)) { - return false; - } - return Objects.equals(password, that.password); + 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() { - int result = host != null ? host.hashCode() : 0; - result = 31 * result + (port != null ? port.hashCode() : 0); - result = 31 * result + (username != null ? username.hashCode() : 0); - result = 31 * result + (password != null ? password.hashCode() : 0); - return result; + return Objects.hash(host, port, username, password); } @Override From 494e2938ed81570272354c433754a8c454038f39 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Tue, 29 Aug 2023 15:12:20 -0700 Subject: [PATCH 09/27] Apply suggestions from code review Co-authored-by: Valentin Kovalenko --- .../src/main/com/mongodb/internal/connection/SocksSocket.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java index 4eb2592d275..a99c53be410 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java @@ -103,6 +103,8 @@ public void connect(final SocketAddress endpoint) throws IOException { @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; From de7697d1783d54ed783a8b0bdf6913616118c9d1 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Tue, 29 Aug 2023 16:10:55 -0700 Subject: [PATCH 10/27] Update driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java Co-authored-by: Valentin Kovalenko --- .../src/main/com/mongodb/internal/connection/SocksSocket.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java index a99c53be410..e943b9b720d 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java @@ -307,7 +307,7 @@ private static int remainingMillis(final Timeout timeout) throws IOException { return 0; } - final int remaining = (int) timeout.remaining(TimeUnit.MILLISECONDS); + final int remaining = Math.toIntExact(timeout.remaining(TimeUnit.MILLISECONDS)); if (remaining > 0) { return remaining; } From 3e84f5c31f3b66f2478ce9d13fc4254a9f6954ac Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 30 Aug 2023 19:45:01 -0700 Subject: [PATCH 11/27] Revert proxy settings for key Management Service. PR fixes. JAVA-4347 --- .evergreen/run-socks5-tests.sh | 23 +- THIRD-PARTY-NOTICES | 3 +- config/spotbugs/exclude.xml | 7 - .../com/mongodb/AutoEncryptionSettings.java | 45 -- .../com/mongodb/ClientEncryptionSettings.java | 39 - .../main/com/mongodb/ConnectionString.java | 86 ++- .../com/mongodb/connection/ProxySettings.java | 136 +++- .../mongodb/connection/SocketSettings.java | 4 +- .../internal/connection/DomainNameUtils.java | 30 + ...etAddresses.java => InetAddressUtils.java} | 4 +- .../internal/connection/SocketStream.java | 36 +- .../internal/connection/SocksSocket.java | 700 +++++++++++++----- .../mongodb/client/model/AggregatesTest.java | 32 +- .../com/mongodb/internal/SocksSocketTest.java | 191 ----- .../connection/DomainNameUtilsTest.java | 65 ++ .../connection/InetAddressUtilsTest.java | 240 ++++++ .../ConnectionStringSpecification.groovy | 60 +- .../MongoClientSettingsSpecification.groovy | 43 ++ .../unit/com/mongodb/ProxySettingsTest.java | 103 ++- .../SocketSettingsSpecification.groovy | 64 +- .../reactivestreams/client/MongoClients.java | 6 +- .../scala/connection/ProxySettings.scala | 10 +- .../mongodb/scala/connection/package.scala | 13 +- .../com/mongodb/client/internal/Crypts.java | 15 +- .../client/internal/KeyManagementService.java | 45 +- .../com/mongodb/client/Socks5ProseTest.java | 93 +-- 26 files changed, 1394 insertions(+), 699 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java rename driver-core/src/main/com/mongodb/internal/connection/{InetAddresses.java => InetAddressUtils.java} (99%) delete mode 100644 driver-core/src/test/functional/com/mongodb/internal/SocksSocketTest.java create mode 100644 driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java create mode 100644 driver-core/src/test/functional/com/mongodb/internal/connection/InetAddressUtilsTest.java diff --git a/.evergreen/run-socks5-tests.sh b/.evergreen/run-socks5-tests.sh index eb2a0533b5e..32bdf95a144 100644 --- a/.evergreen/run-socks5-tests.sh +++ b/.evergreen/run-socks5-tests.sh @@ -43,25 +43,6 @@ provision_ssl () { export GRADLE_SSL_VARS="-Pssl.enabled=true -Pssl.keyStoreType=pkcs12 -Pssl.keyStore=`pwd`/client.pkc -Pssl.keyStorePassword=bithere -Pssl.trustStoreType=jks -Pssl.trustStore=`pwd`/mongo-truststore -Pssl.trustStorePassword=changeit" } -run_csfle_tests () { - local MONGODB_URI=$1 # Get MongoDB URI from the first argument - echo "Running CSFE tests with Java ${JAVA_VERSION} for $TOPOLOGY and connecting to $MONGODB_URI with socks5" - # By not specifying the path to the `crypt_shared` via the `org.mongodb.test.crypt.shared.lib.path` Java system property, - # we force the driver to start `mongocryptd` instead of loading and using `crypt_shared`. - ./gradlew -PjavaVersion=${JAVA_VERSION} -Dorg.mongodb.test.uri=${MONGODB_URI} \ - -Dorg.mongodb.test.fle.on.demand.credential.test.failure.enabled="true" \ - -Dorg.mongodb.test.fle.on.demand.credential.test.azure.keyVaultEndpoint="${AZUREKMS_KEY_VAULT_ENDPOINT}" \ - -Dorg.mongodb.test.fle.on.demand.credential.test.azure.keyName="${AZUREKMS_KEY_NAME}" \ - -Dorg.mongodb.test.awsAccessKeyId=${AWS_ACCESS_KEY_ID} -Dorg.mongodb.test.awsSecretAccessKey=${AWS_SECRET_ACCESS_KEY} \ - -Dorg.mongodb.test.tmpAwsAccessKeyId=${AWS_TEMP_ACCESS_KEY_ID} -Dorg.mongodb.test.tmpAwsSecretAccessKey=${AWS_TEMP_SECRET_ACCESS_KEY} -Dorg.mongodb.test.tmpAwsSessionToken=${AWS_TEMP_SESSION_TOKEN} \ - -Dorg.mongodb.test.azureTenantId=${AZURE_TENANT_ID} -Dorg.mongodb.test.azureClientId=${AZURE_CLIENT_ID} -Dorg.mongodb.test.azureClientSecret=${AZURE_CLIENT_SECRET} \ - -Dorg.mongodb.test.gcpEmail=${GCP_EMAIL} -Dorg.mongodb.test.gcpPrivateKey=${GCP_PRIVATE_KEY} \ - ${GRADLE_SSL_VARS} \ - --stacktrace --info --continue \ - driver-sync:test \ - --tests "*Client*Encryption*" -} - run_socks5_prose_tests () { local PROXY_PORT=$1 local AUTH_ENABLED=$2 @@ -92,7 +73,6 @@ SOCKS5_SERVER_PID_1=$! trap "kill $SOCKS5_SERVER_PID_1" EXIT run_socks5_prose_tests "1080" "true" -run_csfle_tests "$MONGODB_URI&proxyHost=127.0.0.1&proxyPort=1080&proxyUsername=username&proxyPassword=p4ssw0rd" # Second, test with Socks5 + no authentication echo "Running tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connecting to $MONGODB_URI with socks5 auth disabled" @@ -102,5 +82,4 @@ echo "Running tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connec SOCKS5_SERVER_PID_2=$! trap "kill $SOCKS5_SERVER_PID_1; kill $SOCKS5_SERVER_PID_2" EXIT -run_socks5_prose_tests "1081" "false" -run_csfle_tests "$MONGODB_URI&proxyHost=127.0.0.1&proxyPort=1081" \ No newline at end of file +run_socks5_prose_tests "1081" "false" \ No newline at end of file diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 6fead4d514c..971643143b8 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -157,7 +157,8 @@ https://github.com/mongodb/mongo-java-driver. 8) The following files (originally from https://github.com/google/guava): - InetAddresses.java.java + InetAddressUtils.java (formerly InetAddresses.java) + InetAddressUtilsTest.java (formerly InetAddressesTest.java) Copyright 2008-present MongoDB, Inc. Copyright (C) 2008 The Guava Authors diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index 43db87eb91b..d35f0a81c8a 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -196,13 +196,6 @@
- - - - - - - diff --git a/driver-core/src/main/com/mongodb/AutoEncryptionSettings.java b/driver-core/src/main/com/mongodb/AutoEncryptionSettings.java index 00d77f7fb28..1e2be618150 100644 --- a/driver-core/src/main/com/mongodb/AutoEncryptionSettings.java +++ b/driver-core/src/main/com/mongodb/AutoEncryptionSettings.java @@ -17,8 +17,6 @@ package com.mongodb; import com.mongodb.annotations.NotThreadSafe; -import com.mongodb.connection.ProxySettings; -import com.mongodb.connection.SocketSettings; import com.mongodb.lang.Nullable; import org.bson.BsonDocument; @@ -75,14 +73,6 @@ public final class AutoEncryptionSettings { private final boolean bypassAutoEncryption; private final Map encryptedFieldsMap; private final boolean bypassQueryAnalysis; - /** - * Proxy setting used for connecting to Key Management Service via a SOCKS5 proxy server. - *

- * NOTE: If this field is left unset, the default behavior is to utilize the {@link SocketSettings#getProxySettings()} - * from the {@link MongoClientSettings} where the {@link AutoEncryptionSettings} are specified. - */ - @Nullable - private final ProxySettings proxySettings; /** * A builder for {@code AutoEncryptionSettings} so that {@code AutoEncryptionSettings} can be immutable, and to support easier @@ -100,10 +90,6 @@ public static final class Builder { private boolean bypassAutoEncryption; private Map encryptedFieldsMap = Collections.emptyMap(); private boolean bypassQueryAnalysis; - /** - * Proxy setting used for connecting to Key Management Service via a SOCKS5 proxy server. - */ - private ProxySettings proxySettings; /** * Sets the key vault settings. @@ -117,24 +103,6 @@ public Builder keyVaultMongoClientSettings(final MongoClientSettings keyVaultMon return this; } - /** - * Sets the {@link ProxySettings} used for connecting to Key Management Service via a SOCKS5 proxy server. - * - *

- * NOTE: If this field is left unset, the default behavior is to utilize the {@link SocketSettings#getProxySettings()} - * from the {@link MongoClientSettings} where the {@link AutoEncryptionSettings} are specified. - * - * @param proxySettings {@link ProxySettings} to set. - * @return this. - * @see #getProxySettings() - * @since 4.11 - */ - public Builder proxySettings(final ProxySettings proxySettings) { - notNull("proxySettings", proxySettings); - this.proxySettings = proxySettings; - return this; - } - /** * Sets the key vault namespace * @@ -307,18 +275,6 @@ public MongoClientSettings getKeyVaultMongoClientSettings() { return keyVaultMongoClientSettings; } - /** - * Gets the proxy settings used for connecting to Key Management Service via a SOCKS5 proxy server. - * - * @return The {@link ProxySettings} instance containing the SOCKS5 proxy configuration. - * @see Builder#proxySettings(ProxySettings) - * @since 4.11 - */ - @Nullable - public ProxySettings getProxySettings() { - return proxySettings; - } - /** * Gets the key vault namespace. * @@ -537,7 +493,6 @@ private AutoEncryptionSettings(final Builder builder) { this.bypassAutoEncryption = builder.bypassAutoEncryption; this.encryptedFieldsMap = builder.encryptedFieldsMap; this.bypassQueryAnalysis = builder.bypassQueryAnalysis; - this.proxySettings = builder.proxySettings; } @Override diff --git a/driver-core/src/main/com/mongodb/ClientEncryptionSettings.java b/driver-core/src/main/com/mongodb/ClientEncryptionSettings.java index 6de131308ad..2df4b3363d4 100644 --- a/driver-core/src/main/com/mongodb/ClientEncryptionSettings.java +++ b/driver-core/src/main/com/mongodb/ClientEncryptionSettings.java @@ -17,8 +17,6 @@ package com.mongodb; import com.mongodb.annotations.NotThreadSafe; -import com.mongodb.connection.ProxySettings; -import com.mongodb.lang.Nullable; import javax.net.ssl.SSLContext; import java.util.HashMap; @@ -44,12 +42,6 @@ public final class ClientEncryptionSettings { private final Map> kmsProviders; private final Map>> kmsProviderPropertySuppliers; private final Map kmsProviderSslContextMap; - /** - * Proxy setting used for connecting to Key Management Service via a SOCKS5 proxy server. - */ - @Nullable - private final ProxySettings proxySettings; - /** * A builder for {@code ClientEncryptionSettings} so that {@code ClientEncryptionSettings} can be immutable, and to support easier * construction through chaining. @@ -61,10 +53,6 @@ public static final class Builder { private Map> kmsProviders; private Map>> kmsProviderPropertySuppliers = new HashMap<>(); private Map kmsProviderSslContextMap = new HashMap<>(); - /** - * Proxy setting used for connecting to Key Management Service via a SOCKS5 proxy server. - */ - private ProxySettings proxySettings; /** * Sets the {@link MongoClientSettings} that will be used to access the key vault. @@ -78,20 +66,6 @@ public Builder keyVaultMongoClientSettings(final MongoClientSettings keyVaultMon return this; } - /** - * Sets the {@link ProxySettings} used for connecting to Key Management Service via a SOCKS5 proxy server. - * - * @param proxySettings {@link ProxySettings} to set. - * @return this. - * @see #getProxySettings() - * @since 4.11 - */ - public Builder proxySettings(final ProxySettings proxySettings) { - notNull("proxySettings", proxySettings); - this.proxySettings = proxySettings; - return this; - } - /** * Sets the key vault namespace * @@ -177,18 +151,6 @@ public MongoClientSettings getKeyVaultMongoClientSettings() { return keyVaultMongoClientSettings; } - /** - * Gets the proxy settings used for connecting to Key Management Service via a SOCKS5 proxy server. - * - * @return The {@link ProxySettings} instance containing the SOCKS5 proxy configuration. - * @see Builder#proxySettings(ProxySettings) - * @since 4.11 - */ - @Nullable - public ProxySettings getProxySettings() { - return proxySettings; - } - /** * Gets the key vault namespace. *

@@ -297,7 +259,6 @@ private ClientEncryptionSettings(final Builder builder) { this.kmsProviders = notNull("kmsProviders", builder.kmsProviders); this.kmsProviderPropertySuppliers = notNull("kmsProviderPropertySuppliers", builder.kmsProviderPropertySuppliers); this.kmsProviderSslContextMap = notNull("kmsProviderSslContextMap", builder.kmsProviderSslContextMap); - this.proxySettings = builder.proxySettings; } } diff --git a/driver-core/src/main/com/mongodb/ConnectionString.java b/driver-core/src/main/com/mongodb/ConnectionString.java index beb1a825a4b..e360adbb6f9 100644 --- a/driver-core/src/main/com/mongodb/ConnectionString.java +++ b/driver-core/src/main/com/mongodb/ConnectionString.java @@ -128,6 +128,15 @@ *

  • {@code maxIdleTimeMS=ms}: Maximum idle time of a pooled connection. A connection that exceeds this limit will be closed
  • *
  • {@code maxLifeTimeMS=ms}: Maximum life time of a pooled connection. A connection that exceeds this limit will be closed
  • * + *

    Proxy Configuration:

    + *
      + *
    • {@code proxyHost=string}: The SOCKS5 proxy host to establish a connection through. + * It can be provided as a valid IPv4 address, IPv6 address, or a domain name.
    • + *
    • {@code proxyPort=n}: The port number for the SOCKS5 proxy server. Must be a non-negative integer. + * Required if proxyHost is specified.
    • + *
    • {@code proxyUsername=string}: Username for authenticating with the proxy server. Required if proxyPassword is specified.
    • + *
    • {@code proxyPassword=string}: Password for authenticating with the proxy server. Required if proxyUsername is specified.
    • + *
    *

    Connection pool configuration:

    *
      *
    • {@code maxPoolSize=n}: The maximum number of connections in the connection pool.
    • @@ -604,6 +613,18 @@ private void translateOptions(final Map> 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; @@ -619,18 +640,6 @@ private void translateOptions(final Map> optionsMap) { case "ssl": initializeSslEnabled("ssl", value); break; - case "proxyhost": - proxyHost = value; - break; - case "proxyport": - proxyPort = parseInteger(value, "proxyPort"); - break; - case "proxyusername": - proxyUsername = value; - break; - case "proxypassword": - proxyPassword = value; - break; case "tls": initializeSslEnabled("tls", value); break; @@ -1185,14 +1194,24 @@ private void validateProxyParameters() { throw new IllegalArgumentException("proxyPassword can only be specified with proxyHost"); } } - if (proxyPort != null && proxyPort < 0) { - throw new IllegalArgumentException("proxyPort should be equal or greater than 0"); + if (proxyPort != null && (proxyPort < 0 || proxyPort > 65535)) { + throw new IllegalArgumentException("proxyPort should be within the valid range (0 to 65535)"); } - if (proxyUsername != null && proxyUsername.isEmpty()) { - throw new IllegalArgumentException("proxyUsername cannot be empty"); + 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 && proxyPassword.isEmpty()) { - throw new IllegalArgumentException("proxyPassword cannot be empty"); + 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( @@ -1488,21 +1507,45 @@ 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; @@ -1628,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) @@ -1647,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/connection/ProxySettings.java b/driver-core/src/main/com/mongodb/connection/ProxySettings.java index e9c9891c5c7..21ed69040a9 100644 --- a/driver-core/src/main/com/mongodb/connection/ProxySettings.java +++ b/driver-core/src/main/com/mongodb/connection/ProxySettings.java @@ -16,24 +16,37 @@ package com.mongodb.connection; +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; /** - * An immutable class representing settings for connecting to MongoDB via a SOCKS5 proxy server. - * NOTE: This setting is only applicable to the synchronous variant of MongoClient and Key Management Service settings. + * 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 ClientEncryptionSettings#getKeyVaultMongoClientSettings()}. * @since 4.11 */ @Immutable public final class ProxySettings { + private static final int DEFAULT_PORT = 1080; @Nullable private final String host; @@ -49,7 +62,6 @@ public final class ProxySettings { * Creates a {@link Builder} for creating a new {@link ProxySettings} instance. * * @return a new {@link Builder} for {@link ProxySettings}. - * @since 4.11 */ public static ProxySettings.Builder builder() { return new ProxySettings.Builder(); @@ -60,7 +72,6 @@ public static ProxySettings.Builder builder() { * * @param proxySettings existing {@link ProxySettings} to default the builder settings on. * @return a new {@link Builder} for {@link ProxySettings}. - * @since 4.11 */ public static ProxySettings.Builder builder(final ProxySettings proxySettings) { return builder().applySettings(proxySettings); @@ -78,6 +89,16 @@ public static final class Builder { 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; @@ -87,30 +108,84 @@ public ProxySettings.Builder applySettings(final ProxySettings proxySettings) { 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) + */ public ProxySettings.Builder host(final String host) { notNull("proxyHost", host); - isTrue("proxyHost is not empty", host.trim().length() > 0); + 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) + */ public ProxySettings.Builder port(final int port) { - isTrue("proxyPort is equal or greater than 0", port >= 0); + 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) + */ public ProxySettings.Builder username(final String username) { notNull("username", username); - isTrue("username is not empty", !username.isEmpty()); + 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) + */ public ProxySettings.Builder password(final String password) { notNull("password", password); - isTrue("password is not empty", !password.isEmpty()); + 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; } @@ -160,26 +235,63 @@ public ProxySettings build() { } } + /** + * Gets the SOCKS5 proxy host. + * + * @return the proxy host value. + * @see Builder#host(String) + */ @Nullable public String getHost() { return host; } - @Nullable - public Integer getPort() { - return port; + /** + * 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) { diff --git a/driver-core/src/main/com/mongodb/connection/SocketSettings.java b/driver-core/src/main/com/mongodb/connection/SocketSettings.java index 764df35baf2..409797dc39a 100644 --- a/driver-core/src/main/com/mongodb/connection/SocketSettings.java +++ b/driver-core/src/main/com/mongodb/connection/SocketSettings.java @@ -36,9 +36,6 @@ public final class SocketSettings { private final long readTimeoutMS; private final int receiveBufferSize; private final int sendBufferSize; - /** - * NOTE: This setting is only applicable to the synchronous variant of MongoClient. - */ private final ProxySettings proxySettings; /** @@ -214,6 +211,7 @@ public int getReadTimeout(final TimeUnit timeUnit) { * * @return The {@link ProxySettings} instance containing the SOCKS5 proxy configuration. * @see Builder#applyToProxySettings(Block) + * @since 4.11 */ public ProxySettings getProxySettings() { return proxySettings; 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..f45230146a7 --- /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("^(([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/InetAddresses.java b/driver-core/src/main/com/mongodb/internal/connection/InetAddressUtils.java similarity index 99% rename from driver-core/src/main/com/mongodb/internal/connection/InetAddresses.java rename to driver-core/src/main/com/mongodb/internal/connection/InetAddressUtils.java index c882b31d6d9..9d82947671a 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/InetAddresses.java +++ b/driver-core/src/main/com/mongodb/internal/connection/InetAddressUtils.java @@ -29,13 +29,13 @@ * 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 InetAddresses { +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 InetAddresses() { + private InetAddressUtils() { } /** 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 0993ad46267..dd8418bf5a5 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SocketStream.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SocketStream.java @@ -19,10 +19,10 @@ import com.mongodb.MongoSocketException; import com.mongodb.MongoSocketOpenException; import com.mongodb.MongoSocketReadException; -import com.mongodb.connection.ProxySettings; 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; @@ -84,7 +84,7 @@ public void open() { protected Socket initializeSocket() throws IOException { ProxySettings proxySettings = settings.getProxySettings(); - if (proxySettings.getHost() != null) { + if (proxySettings.isProxyEnabled()) { if (sslSettings.isEnabled()) { assertTrue(socketFactory instanceof SSLSocketFactory); SSLSocketFactory sslSocketFactory = (SSLSocketFactory) socketFactory; @@ -110,12 +110,12 @@ protected Socket initializeSocket() throws IOException { } private SSLSocket initializeSslSocketOverSocksProxy(final SSLSocketFactory sslSocketFactory) throws IOException { - String serverHost = address.getHost(); - int serverPort = address.getPort(); + final String serverHost = address.getHost(); + final int serverPort = address.getPort(); - SocksSocket socksProxy = new SocksSocket(null, settings.getProxySettings()); + SocksSocket socksProxy = new SocksSocket(settings.getProxySettings()); configureSocket(socksProxy, settings); - InetSocketAddress inetSocketAddress = InetSocketAddress.createUnresolved(serverHost, serverPort); + InetSocketAddress inetSocketAddress = toSocketAddress(serverHost, serverPort); socksProxy.connect(inetSocketAddress, settings.getConnectTimeout(MILLISECONDS)); SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socksProxy, serverHost, serverPort, true); @@ -125,14 +125,28 @@ private SSLSocket initializeSslSocketOverSocksProxy(final SSLSocketFactory sslSo 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(); - SocksSocket socksProxy = new SocksSocket(createdSocket, settings.getProxySettings()); 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(InetSocketAddress.createUnresolved(address.getHost(), address.getPort()), + socksProxy.connect(toSocketAddress(address.getHost(), address.getPort()), settings.getConnectTimeout(TimeUnit.MILLISECONDS)); - return createdSocket; + return socksProxy; } @Override @@ -214,10 +228,6 @@ SocketSettings getSettings() { return settings; } - SocketFactory getSocketFactory() { - return socketFactory; - } - @Override public void close() { try { diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java index e943b9b720d..48a58cb188c 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java @@ -17,112 +17,96 @@ import com.mongodb.connection.ProxySettings; import com.mongodb.internal.Timeout; -import com.mongodb.internal.VisibleForTesting; 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 java.util.regex.Pattern; +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 Pattern DOMAIN_PATTERN = Pattern.compile("^(([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}|localhost)$"); - private static final byte LENGTH_OF_IPV4 = 4; - private static final byte LENGTH_OF_IPV6 = 16; - private static final byte SOCKS_VERSION = 5; + // private static final byte LENGTH_OF_IPV4 = 4; +// private static final byte LENGTH_OF_IPV6 = 16; + private static final byte SOCKS_VERSION = 0x05; private static final byte RESERVED = 0x00; private static final byte PORT_SIZE = 2; private static final byte AUTHENTICATION_SUCCEEDED_STATUS = 0x00; - private static final byte REQUEST_OK = 0; - private static final byte ADDRESS_TYPE_DOMAIN_NAME = 3; - private static final byte ADDRESS_TYPE_IPV4 = 1; - private static final byte ADDRESS_TYPE_IPV6 = 4; - private static final int DEFAULT_PORT = 1080; public static final String IP_PARSING_ERROR_SUFFIX = " is not an IP string literal"; - private final SocksAuthenticationMethod[] authenticationMethods; - private final InetSocketAddress proxyAddress; + private static final byte USER_PASSWORD_SUB_NEGOTIATION_VERSION = 0x01; private InetSocketAddress remoteAddress; - @Nullable - private String proxyUsername; - @Nullable - private String proxyPassword; + private final ProxySettings proxySettings; @Nullable private final Socket socket; - private InputStream inputStream; - private OutputStream outputStream; public SocksSocket(final ProxySettings proxySettings) { this(null, proxySettings); } public SocksSocket(@Nullable final Socket socket, final ProxySettings proxySettings) { - int port = getPort(proxySettings); - assertTrue(proxySettings.getHost() != null); - assertTrue(port >= 0); - - this.proxyAddress = new InetSocketAddress(proxySettings.getHost(), port); - this.socket = socket; - - if (proxySettings.getUsername() != null && proxySettings.getPassword() != null) { - this.authenticationMethods = new SocksAuthenticationMethod[]{ - SocksAuthenticationMethod.NO_AUTH, - SocksAuthenticationMethod.USERNAME_PASSWORD}; - this.proxyUsername = proxySettings.getUsername(); - this.proxyPassword = proxySettings.getPassword(); - } else { - this.authenticationMethods = new SocksAuthenticationMethod[]{SocksAuthenticationMethod.NO_AUTH}; - } - } - - private static int getPort(final ProxySettings proxySettings) { - if (proxySettings.getPort() != null) { - return proxySettings.getPort(); + 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()); } - return DEFAULT_PORT; - } - - @Override - public void connect(final SocketAddress endpoint) throws IOException { - connect(endpoint, 0); + 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); + // `Socket` requires `IllegalArgumentException` + isTrueArgument("timeoutMs", timeoutMs >= 0); try { Timeout timeout = toTimeout(timeoutMs); InetSocketAddress unresolvedAddress = (InetSocketAddress) endpoint; assertTrue(unresolvedAddress.isUnresolved()); this.remoteAddress = unresolvedAddress; - if (socket != null && !socket.isConnected()) { + + InetSocketAddress proxyAddress = new InetSocketAddress(proxySettings.getHost(), proxySettings.getPort()); + if (socket != null) { socket.connect(proxyAddress, remainingMillis(timeout)); - inputStream = socket.getInputStream(); - outputStream = socket.getOutputStream(); } else { super.connect(proxyAddress, remainingMillis(timeout)); - inputStream = getInputStream(); - outputStream = getOutputStream(); } SocksAuthenticationMethod authenticationMethod = performHandshake(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; } @@ -132,167 +116,178 @@ private void sendConnect(final Timeout timeout) throws IOException { final String host = remoteAddress.getHostName(); final int port = remoteAddress.getPort(); final byte[] bytesOfHost = host.getBytes(StandardCharsets.UTF_8); - final int hostLength = host.length(); + final int hostLength = bytesOfHost.length; - byte addressType; + AddressType addressType; byte[] ipAddress = null; if (isDomainName(host)) { - addressType = ADDRESS_TYPE_DOMAIN_NAME; + addressType = DOMAIN_NAME; } else { ipAddress = createByteArrayFromIpAddress(host); addressType = determineAddressType(ipAddress); } byte[] bufferSent = createBuffer(addressType, hostLength); bufferSent[0] = SOCKS_VERSION; - bufferSent[1] = (byte) SocksCommand.CONNECT.getCommandNumber(); + bufferSent[1] = SocksCommand.CONNECT.getCommandNumber(); bufferSent[2] = RESERVED; switch (addressType) { - case ADDRESS_TYPE_DOMAIN_NAME: - bufferSent[3] = ADDRESS_TYPE_DOMAIN_NAME; + case DOMAIN_NAME: + bufferSent[3] = DOMAIN_NAME.getAddressTypeNumber(); bufferSent[4] = (byte) hostLength; System.arraycopy(bytesOfHost, 0, bufferSent, 5, hostLength); - bufferSent[5 + host.length()] = (byte) ((port & 0xff00) >> 8); - bufferSent[6 + host.length()] = (byte) (port & 0xff); + addPort(bufferSent, 5 + hostLength, port); break; - case ADDRESS_TYPE_IPV4: - bufferSent[3] = ADDRESS_TYPE_IPV4; + case IP_V4: + bufferSent[3] = IP_V4.getAddressTypeNumber(); System.arraycopy(ipAddress, 0, bufferSent, 4, ipAddress.length); - bufferSent[4 + ipAddress.length] = (byte) ((port & 0xff00) >> 8); - bufferSent[5 + ipAddress.length] = (byte) (port & 0xff); + addPort(bufferSent, 4 + ipAddress.length, port); break; - case ADDRESS_TYPE_IPV6: - bufferSent[3] = ADDRESS_TYPE_IPV6; + case IP_V6: + bufferSent[3] = DOMAIN_NAME.getAddressTypeNumber(); System.arraycopy(ipAddress, 0, bufferSent, 4, ipAddress.length); - bufferSent[4 + ipAddress.length] = (byte) ((port & 0xff00) >> 8); - bufferSent[5 + ipAddress.length] = (byte) (port & 0xff); + addPort(bufferSent, 4 + ipAddress.length, port); break; default: fail(); } + OutputStream outputStream = getOutputStream(); outputStream.write(bufferSent); outputStream.flush(); checkServerReply(timeout); } - @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) - public static boolean isDomainName(final String host) { - return DOMAIN_PATTERN.matcher(host).matches(); + private static void addPort(final byte[] bufferSent, final int index, final int port) { + bufferSent[index] = (byte) ((port & 0xff00) >> 8); + bufferSent[index + 1] = (byte) (port & 0xff); } - @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) - public static byte[] createByteArrayFromIpAddress(final String host) throws SocketException { - byte[] bytes = InetAddresses.ipStringToBytes(host); + 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 byte determineAddressType(final byte[] ipAddress) { - if (ipAddress.length == LENGTH_OF_IPV4) { - return ADDRESS_TYPE_IPV4; - } else if (ipAddress.length == LENGTH_OF_IPV6) { - return ADDRESS_TYPE_IPV6; + 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 byte addressType, final int hostLength) { + private static byte[] createBuffer(final AddressType addressType, final int hostLength) { switch (addressType) { - case ADDRESS_TYPE_DOMAIN_NAME: + case DOMAIN_NAME: return new byte[7 + hostLength]; - case ADDRESS_TYPE_IPV4: - return new byte[6 + LENGTH_OF_IPV4]; - case ADDRESS_TYPE_IPV6: - return new byte[6 + LENGTH_OF_IPV6]; + case IP_V4: + return new byte[6 + IP_V4.getLength()]; + case IP_V6: + return new byte[6 + IP_V6.getLength()]; default: - break; + throw fail(); } - throw fail(); } private void checkServerReply(final Timeout timeout) throws IOException { - byte[] data = readSocksReply(inputStream, 4, timeout); - byte status = data[1]; - - if (status == REQUEST_OK) { - switch (data[3]) { - case ADDRESS_TYPE_IPV4: - readSocksReply(inputStream, 4 + PORT_SIZE, timeout); + 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_SIZE, timeout); break; - case ADDRESS_TYPE_DOMAIN_NAME: - byte hostNameLength = readSocksReply(inputStream, 1, timeout)[0]; - readSocksReply(inputStream, hostNameLength + PORT_SIZE, timeout); + case IP_V4: + readSocksReply(IP_V4.getLength() + PORT_SIZE, timeout); break; - case ADDRESS_TYPE_IPV6: - readSocksReply(inputStream, 16 + PORT_SIZE, timeout); + case IP_V6: + readSocksReply(IP_V6.getLength() + PORT_SIZE, timeout); break; default: - throw new ConnectException("Reply from SOCKS proxy server contains wrong code"); + throw fail(); } return; } - - for (ServerErrorReply serverErrorReply : ServerErrorReply.values()) { - if (status == serverErrorReply.getReplyNumber()) { - throw new ConnectException(serverErrorReply.getMessage()); - } - } - throw new ConnectException("Unknown status"); + 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(proxyUsername).getBytes(StandardCharsets.UTF_8); - final byte[] bytesOfPassword = assertNotNull(proxyPassword).getBytes(StandardCharsets.UTF_8); + 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] = 0x01; + 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[] authenticationResult = readSocksReply(inputStream, 2, timeout); + byte[] authResult = readSocksReply(2, timeout); + byte authStatus = authResult[1]; - if (authenticationResult[1] != AUTHENTICATION_SUCCEEDED_STATUS) { - /* RFC 1929 specifies that the connection MUST be closed if - authentication fails */ - throw new ConnectException("Authentication failed"); + if (authStatus != AUTHENTICATION_SUCCEEDED_STATUS) { + throw new ConnectException("Authentication failed. Server returned status: " + authStatus); } } } private SocksAuthenticationMethod performHandshake(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] = (byte) authenticationMethods[i].getMethodNumber(); + bufferSent[2 + i] = authenticationMethods[i].getMethodNumber(); } + + OutputStream outputStream = getOutputStream(); outputStream.write(bufferSent); outputStream.flush(); - byte[] handshakeReply = readSocksReply(inputStream, 2, timeout); + byte[] handshakeReply = readSocksReply(2, timeout); if (handshakeReply[0] != SOCKS_VERSION) { - throw new ConnectException("Remote server doesn't support SOCKS5"); + throw new ConnectException("Remote server doesn't support socks version 5" + + " Received version: " + handshakeReply[0]); } - if (handshakeReply[1] == (byte) 0xFF) { - throw new ConnectException("None of the authentication methods listed are acceptable"); + 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 (handshakeReply[1] == SocksAuthenticationMethod.NO_AUTH.getMethodNumber()) { + if (authMethodNumber == SocksAuthenticationMethod.NO_AUTH.getMethodNumber()) { return SocksAuthenticationMethod.NO_AUTH; + } else if (authMethodNumber == SocksAuthenticationMethod.USERNAME_PASSWORD.getMethodNumber()) { + return SocksAuthenticationMethod.USERNAME_PASSWORD; } - 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) { @@ -315,7 +310,8 @@ private static int remainingMillis(final Timeout timeout) throws IOException { throw new SocketTimeoutException("Socket connection timed out"); } - private byte[] readSocksReply(final InputStream in, final int length, final Timeout timeout) throws IOException { + 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(); @@ -324,11 +320,7 @@ private byte[] readSocksReply(final InputStream in, final int length, final Time int count; int remaining = remainingMillis(timeout); setSoTimeout(remaining); - try { - count = in.read(data, received, length - received); - } catch (SocketTimeoutException e) { - throw new SocketTimeoutException("Socket connection timed out"); - } + count = inputStream.read(data, received, length - received); if (count < 0) { throw new ConnectException("Malformed reply from SOCKS proxy server"); } @@ -340,29 +332,127 @@ private byte[] readSocksReply(final InputStream in, final int length, final Time return data; } - public static byte[] read(final InputStream inputStream, final int length) throws IOException { - byte[] bytes = new byte[length]; - for (int i = 0; i < length; i++) { - int read = inputStream.read(); - if (read < 0) { - throw new ConnectException("End of stream"); + 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(); } - bytes[i] = (byte) read; + }; + + 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, "Proxy server general failure"), + NET_UNREACHABLE(0x03, "Connection not allowed by ruleset"), + HOST_UNREACHABLE(0x04, "Network is unreachable"), + CONN_REFUSED(0x05, "Host is unreachable"), + TTL_EXPIRED(0x06, "Connection has been refused"), + CMD_NOT_SUPPORTED(0x07, "TTL expired"), + ADDR_TYPE_NOT_SUP(0x08, "Address type 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.getReplyNumber()) { + return serverReply; + } + } + + throw new ConnectException("Unknown reply field. Reply field: " + status); + } + + public int getReplyNumber() { + return replyNumber; + } + + public String getMessage() { + return message; } - return bytes; } @Override - public synchronized void close() throws IOException { - if (socket != null) { - socket.close(); - } else { + 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 synchronized void setSoTimeout(final int timeout) throws SocketException { + public void setSoTimeout(final int timeout) throws SocketException { if (socket != null) { socket.setSoTimeout(timeout); } else { @@ -371,76 +461,324 @@ public synchronized void setSoTimeout(final int timeout) throws SocketException } @Override - public InputStream getInputStream() throws IOException { + public int getSoTimeout() throws SocketException { if (socket != null) { - return socket.getInputStream(); + return socket.getSoTimeout(); + } else { + return super.getSoTimeout(); } - return super.getInputStream(); } @Override - public OutputStream getOutputStream() throws IOException { + public void bind(final SocketAddress bindpoint) throws IOException { if (socket != null) { - return socket.getOutputStream(); + socket.bind(bindpoint); + } else { + super.bind(bindpoint); } - return super.getOutputStream(); } - private enum SocksCommand { + @Override + public InetAddress getInetAddress() { + if (socket != null) { + return socket.getInetAddress(); + } else { + return super.getInetAddress(); + } + } - CONNECT(0x01); + @Override + public InetAddress getLocalAddress() { + if (socket != null) { + return socket.getLocalAddress(); + } else { + return super.getLocalAddress(); + } + } - private final int value; + @Override + public int getPort() { + if (socket != null) { + return socket.getPort(); + } else { + return super.getPort(); + } + } - SocksCommand(final int value) { - this.value = value; + @Override + public int getLocalPort() { + if (socket != null) { + return socket.getLocalPort(); + } else { + return super.getLocalPort(); } + } - public int getCommandNumber() { - return value; + @Override + public SocketAddress getRemoteSocketAddress() { + if (socket != null) { + return socket.getRemoteSocketAddress(); + } else { + return super.getRemoteSocketAddress(); } } - private enum SocksAuthenticationMethod { - NO_AUTH(0x00), - USERNAME_PASSWORD(0x02); + @Override + public SocketAddress getLocalSocketAddress() { + if (socket != null) { + return socket.getLocalSocketAddress(); + } else { + return super.getLocalSocketAddress(); + } + } - private final int methodNumber; + @Override + public SocketChannel getChannel() { + if (socket != null) { + return socket.getChannel(); + } else { + return super.getChannel(); + } + } - SocksAuthenticationMethod(final int methodNumber) { - this.methodNumber = methodNumber; + @Override + public void setTcpNoDelay(final boolean on) throws SocketException { + if (socket != null) { + socket.setTcpNoDelay(on); + } else { + super.setTcpNoDelay(on); } + } - public int getMethodNumber() { - return methodNumber; + @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); + } } - private enum ServerErrorReply { - GENERAL_FAILURE(1, "Remote server doesn't support SOCKS5"), - NOT_ALLOWED(2, "Proxy server general failure"), - NET_UNREACHABLE(3, "Connection not allowed by ruleset"), - HOST_UNREACHABLE(4, "Network is unreachable"), - CONN_REFUSED(5, "Host is unreachable"), - TTL_EXPIRED(6, "Connection has been refused"), - CMD_NOT_SUPPORTED(7, "TTL expired"), - ADDR_TYPE_NOT_SUP(8, "Address type not supported"); + @Override + public int getSoLinger() throws SocketException { + if (socket != null) { + return socket.getSoLinger(); + } else { + return super.getSoLinger(); + } + } - private final int replyNumber; - private final String message; + @Override + public void sendUrgentData(final int data) throws IOException { + if (socket != null) { + socket.sendUrgentData(data); + } else { + super.sendUrgentData(data); + } + } - ServerErrorReply(final int replyNumber, final String message) { - this.replyNumber = replyNumber; - this.message = message; + @Override + public void setOOBInline(final boolean on) throws SocketException { + if (socket != null) { + socket.setOOBInline(on); + } else { + super.setOOBInline(on); } + } - public int getReplyNumber() { - return replyNumber; + @Override + public boolean getOOBInline() throws SocketException { + if (socket != null) { + return socket.getOOBInline(); + } else { + return super.getOOBInline(); } + } - public String getMessage() { - return message; + @Override + public synchronized void setSendBufferSize(final int size) throws SocketException { + if (socket != null) { + socket.setSendBufferSize(size); + } else { + super.setSendBufferSize(size); } } + + @Override + public synchronized int getSendBufferSize() throws SocketException { + if (socket != null) { + return socket.getSendBufferSize(); + } else { + return super.getSendBufferSize(); + } + } + + @Override + public synchronized void setReceiveBufferSize(final int size) throws SocketException { + if (socket != null) { + socket.setReceiveBufferSize(size); + } else { + super.setReceiveBufferSize(size); + } + } + + @Override + public synchronized 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/test/functional/com/mongodb/client/model/AggregatesTest.java b/driver-core/src/test/functional/com/mongodb/client/model/AggregatesTest.java index 5ed5ac7cf32..713e4c3f7b6 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/AggregatesTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/AggregatesTest.java @@ -24,6 +24,7 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -54,8 +55,10 @@ public class AggregatesTest extends OperationTest { private static Stream groupWithQuantileSource() { return Stream.of( - Arguments.of(percentile("result", "$x", MqlValues.ofNumberArray(0.95), QuantileMethod.approximate()), asList(3.0), asList(1.0)), - Arguments.of(percentile("result", "$x", MqlValues.ofNumberArray(0.95, 0.3), QuantileMethod.approximate()), asList(3.0, 2.0), asList(1.0, 1.0)), + Arguments.of(percentile("result", "$x", MqlValues.ofNumberArray(0.95), QuantileMethod.approximate()), asList(3.0), + asList(1.0)), + Arguments.of(percentile("result", "$x", MqlValues.ofNumberArray(0.95, 0.3), QuantileMethod.approximate()), asList(3.0, 2.0), + asList(1.0, 1.0)), Arguments.of(median("result", "$x", QuantileMethod.approximate()), 2.0d, 1.0d) ); } @@ -164,6 +167,7 @@ public void testUnset() { } @Test + @Disabled public void testGeoNear() { getCollectionHelper().insertDocuments("[\n" + " {\n" @@ -199,18 +203,18 @@ public void testGeoNear() { )); List pipeline = assertPipeline("{\n" - + " $geoNear: {\n" - + " near: { type: 'Point', coordinates: [ -73.99279 , 40.719296 ] },\n" - + " distanceField: 'dist.calculated',\n" - + " minDistance: 0,\n" - + " maxDistance: 2,\n" - + " query: { category: 'Parks' },\n" - + " includeLocs: 'dist.location',\n" - + " spherical: true,\n" - + " key: 'location',\n" - + " distanceMultiplier: 10.0\n" - + " }\n" - + "}", + + " $geoNear: {\n" + + " near: { type: 'Point', coordinates: [ -73.99279 , 40.719296 ] },\n" + + " distanceField: 'dist.calculated',\n" + + " minDistance: 0,\n" + + " maxDistance: 2,\n" + + " query: { category: 'Parks' },\n" + + " includeLocs: 'dist.location',\n" + + " spherical: true,\n" + + " key: 'location',\n" + + " distanceMultiplier: 10.0\n" + + " }\n" + + "}", geoNear( new Point(new Position(-73.99279, 40.719296)), "dist.calculated", diff --git a/driver-core/src/test/functional/com/mongodb/internal/SocksSocketTest.java b/driver-core/src/test/functional/com/mongodb/internal/SocksSocketTest.java deleted file mode 100644 index fad058fd422..00000000000 --- a/driver-core/src/test/functional/com/mongodb/internal/SocksSocketTest.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * 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; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import java.net.SocketException; - -import static com.mongodb.internal.connection.SocksSocket.createByteArrayFromIpAddress; -import static com.mongodb.internal.connection.SocksSocket.isDomainName; - -class SocksSocketTest { - private static final byte LENGTH_OF_IPV4 = 4; - private static final byte LENGTH_OF_IPV6 = 16; - private static final String IP_PARSING_ERROR_SUFFIX = " is not an IP string literal"; - - @ParameterizedTest - @ValueSource(strings = { - "2001:db8:85a3::8a2e:370:7334", - "::5000", - "5000::", - "1:2:3:4:5:6:7:8", - "0:0:0:0:0:0:0:2", - "1:2:3:4:5:6::7", - "::1:2:3:4:5:6:7", - "1:2:3:4:5:6:7::", - "::2", - "0:000::0:2", - "2001:db8:85a3::8a2e:370:7334", - "1::", - "0::1", - "::0:0000:0", - "::", - "::1", - "0:0:0:0:0:0:0:0", - "0:0:0:0:0:0:0:1", - }) - void shouldReturnIpv6Address(final String ipAddress) throws SocketException { - Assertions.assertEquals(LENGTH_OF_IPV6, createByteArrayFromIpAddress(ipAddress).length); - } - - - @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", - }) - 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", - "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" - }) - void shouldReturnFalseWithInvalidHostName(final String hostname) { - Assertions.assertFalse(isDomainName(hostname)); - } - - @ParameterizedTest - @ValueSource(strings = { - "192.168.0.1", - "10.0.0.1", - "172.16.0.1", - "255.255.255.255", - "127.0.0.1", - "169.254.1.2", - "1.2.3.4" - }) - void shouldReturnIpv4Address(final String ipAddress) throws SocketException { - Assertions.assertEquals(LENGTH_OF_IPV4, createByteArrayFromIpAddress(ipAddress).length); - } - - @ParameterizedTest - @ValueSource(strings = { - //Invalid IPV4 addresses - "256.0.0.1", - "192.168.256.1", - "192.168.0.", - "300.300.300.300", - "192.168.0.0.1", - "110.010.20.030", // octal representation - "008.8.8.8", // octal representation - "007.008.009.010", // octal representation - - //Invalid IPV6 addresses - "::::", - "0:1:2:3:4:5:6::7", - "0::1:2:3:4:5:6:7", - "0:1:2::3:4:5:6:7", - "::1:2:3:4:5:6:7:8", - "1:2:3:4:5:6:7:8::", - "0:1:2:3:4:5:6:7:8:9", - "::5000::", - "5::3::4", - "::5::4::", - "::5::4", - "4::5::", - "1::2:3::4", - "1:2", - "2:::5", - "1::2::5", - ":4:", - ":7", - "7:", - "1", - ":5:2", - "5:2:", - "1:2:3:4:5:6:7", - ":::::", - ":::", - "::n::", - "2001:db8:85a3::8a2e:370:7334:", - ":2001:db8:85a3::8a2e:370:7334", - "20012:db8:85a3::8a2e:370:7334", - "20012:20012:20012:20012:20012:20012:20012:20012", - - //Domain names - "localhost", - "3letter.xyz", - "my_domain.com", - "hyphenated-name.net", - "numbers123.org", - "_underscored.site", - "xn--80ak6aa92e.com (IDN)", - "localhost", - "www.ab--cd.com", - ".invalid", - "example.invalid", - "test.-site.com", - "subdomain..domain.com", - "256charactersinthisdomainnamethatexceedsthemaximumallowedlengthfortld.com", - ".xn--32-6kcakeb6cn8ak4b1d4dkswnqn.xn--pss-3p4d1dm5a.xn--jlq61u9w3b (Punycode)", - "--doublehyphens.org", - "subdomain.toolongtldddddddddddddd", - "spaced out.site", - "no_spaces.domain.com", - "my hostname.com", - "localhost:8080", - "www.example.com.", - "-startingwithhyphen.net", - "this-domain-is-really-long-because-it-just-keeps-going-and-going-and-its-still-not-done-yet-because-theres-more.net", - "sub--sub.domain.com", - "mydomain.", - "www..com", - "subdomain.no_underscores_or_dots_allowed,", - }) - void shouldThrowErrorWhenInvalidIpAddressIsProvided(final String ipAddress) { - SocketException socketException = Assertions.assertThrows(SocketException.class, () -> createByteArrayFromIpAddress(ipAddress)); - Assertions.assertEquals(ipAddress + IP_PARSING_ERROR_SUFFIX, socketException.getMessage()); - } - -} 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..36fbb638db7 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java @@ -0,0 +1,65 @@ +/* + * 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", + }) + 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", + "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" + }) + 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 633fecd0432..3d18f8eb6b8 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'() { @@ -387,19 +388,37 @@ class ConnectionStringSpecification extends Specification { assert exception.message == cause where: - - cause | connectionString - 'proxyPort can only be specified with proxyHost' | 'mongodb://localhost:27017/?proxyPort=1' - 'proxyPort should be equal or greater than 0' | 'mongodb://localhost:27017/?proxyHost=a&proxyPort=-1' - '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=' + 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' + ' They cannot be set individually' | 'mongodb://localhost:27017/?proxyHost=a&proxyPassword=1' + } + + 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: @@ -642,6 +661,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;' @@ -693,8 +722,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 index b868b9ab9fc..e161b25b61c 100644 --- a/driver-core/src/test/unit/com/mongodb/ProxySettingsTest.java +++ b/driver-core/src/test/unit/com/mongodb/ProxySettingsTest.java @@ -18,59 +18,102 @@ 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 { - static Stream shouldThrowExceptionWhenProxySettingAreInInvalid() { + + 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(1080), "state should be: proxyPort can only be specified with proxyHost"), + .port(VALID_PORT), "state should be: proxyPort can only be specified with proxyHost"), Arguments.of(ProxySettings.builder() - .port(1080) - .username("test") - .password("test"), "state should be: proxyPort can only be specified with proxyHost"), + .port(VALID_PORT) + .username(USERNAME) + .password(PASSWORD), "state should be: proxyPort can only be specified with proxyHost"), Arguments.of(ProxySettings.builder() - .username("test"), "state should be: proxyUsername can only be specified with proxyHost"), + .username(USERNAME), "state should be: proxyUsername can only be specified with proxyHost"), Arguments.of(ProxySettings.builder() - .password("test"), "state should be: proxyPassword can only be specified with proxyHost"), + .password(PASSWORD), "state should be: proxyPassword can only be specified with proxyHost"), Arguments.of(ProxySettings.builder() - .host("test") - .username("test"), + .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("test") - .password("test"), + .host(HOST) + .password(PASSWORD), "state should be: Both proxyUsername and proxyPassword must be set together. They cannot be set individually") ); } @ParameterizedTest @MethodSource - void shouldThrowExceptionWhenProxySettingAreInInvalid(final ProxySettings.Builder builder, final String expectedErrorMessage) { + 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("test") - .port(1080)), + .host(HOST) + .port(VALID_PORT)), Arguments.of(ProxySettings.builder() - .host("test")), + .host(HOST)), Arguments.of(ProxySettings.builder() - .host("test") - .port(1080) - .username("test") - .password("test")), + .host(HOST) + .port(VALID_PORT) + .host(USERNAME) + .host(PASSWORD)), Arguments.of(ProxySettings.builder() - .host("test") - .username("test") - .password("test")) + .host(HOST) + .host(USERNAME) + .host(PASSWORD)) ); } @@ -79,4 +122,20 @@ static Stream shouldNotThrowExceptionWhenProxySettingAreValid() { 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 193d0b657be..ac249a0d0d1 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 @@ -110,10 +110,10 @@ 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().getHost() != null){ + throw new MongoClientException("Proxy is not supported for reactive clients"); + } if (settings.getStreamFactoryFactory() == null) { - if (settings.getSocketSettings().getProxySettings().getHost() != null){ - throw new MongoClientException("Proxy is not supported for reactive clients"); - } if (settings.getSslSettings().isEnabled()) { return createWithTlsChannel(settings, mongoDriverInformation); } else { 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 index 745714e264e..e2e501a04b1 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/connection/ProxySettings.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/connection/ProxySettings.scala @@ -19,9 +19,15 @@ package org.mongodb.scala.connection import com.mongodb.connection.{ ProxySettings => JProxySettings } /** - * An immutable class representing settings for connecting to MongoDB via a SOCKS5 proxy server. - * NOTE: This setting is only applicable to the synchronous variant of MongoClient and Key Management Service settings. + * 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 [[SocketSettings#getProxySettings]] + * @see [[org.mongodb.scala.AutoEncryptionSettings#getKeyVaultMongoClientSettings]] + * @see [[org.mongodb.scala.ClientEncryptionSettings#getKeyVaultMongoClientSettings]] * @since 4.11 */ object ProxySettings { 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 17d44ca5657..bcbfd89ec07 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 @@ -42,8 +42,17 @@ package object connection { type SocketSettings = com.mongodb.connection.SocketSettings /** - * Settings for connecting to MongoDB via proxy server. - * NOTE: This setting is only applicable to the synchronous variant of MongoClient and Key Management Service settings. + * This setting is only applicable when communicating with a MongoDB server or a Key Management Service + * using the synchronous variant of `com.mongodb.MongoClient` or `ClientEncryption`. + * + * This setting is furthermore ignored if: + * - the communication is via `com.mongodb.UnixServerAddress` (Unix domain socket). + * - a `StreamFactoryFactory` is `MongoClientSettings.Builder.streamFactoryFactory` configured. + * + * @see [[SocketSettings#getProxySettings]] + * @see [[AutoEncryptionSettings#getProxySettings]] + * @see [[ClientEncryptionSettings#getProxySettings]] + * @since 4.11 */ type ProxySettings = com.mongodb.connection.ProxySettings diff --git a/driver-sync/src/main/com/mongodb/client/internal/Crypts.java b/driver-sync/src/main/com/mongodb/client/internal/Crypts.java index bd5fb51a45f..73e4d42e8ef 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/Crypts.java +++ b/driver-sync/src/main/com/mongodb/client/internal/Crypts.java @@ -20,12 +20,10 @@ import com.mongodb.ClientEncryptionSettings; import com.mongodb.MongoClientSettings; import com.mongodb.MongoNamespace; -import com.mongodb.connection.ProxySettings; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.crypt.capi.MongoCrypt; import com.mongodb.crypt.capi.MongoCrypts; -import com.mongodb.lang.Nullable; import javax.net.ssl.SSLContext; import java.util.Map; @@ -50,14 +48,10 @@ public static Crypt createCrypt(final MongoClientImpl client, final AutoEncrypti MongoClient keyVaultClient = keyVaultMongoClientSettings == null ? sharedInternalClient : MongoClients.create(keyVaultMongoClientSettings); MongoCrypt mongoCrypt = MongoCrypts.create(createMongoCryptOptions(settings)); - - ProxySettings kmsProxySettings = settings.getProxySettings() == null ? client.getSettings() - .getSocketSettings().getProxySettings() : settings.getProxySettings(); - return new Crypt( mongoCrypt, createKeyRetriever(keyVaultClient, settings.getKeyVaultNamespace()), - createKeyManagementService(settings.getKmsProviderSslContextMap(), kmsProxySettings), + createKeyManagementService(settings.getKmsProviderSslContextMap()), settings.getKmsProviders(), settings.getKmsProviderPropertySuppliers(), settings.isBypassAutoEncryption(), @@ -69,7 +63,7 @@ public static Crypt createCrypt(final MongoClientImpl client, final AutoEncrypti static Crypt create(final MongoClient keyVaultClient, final ClientEncryptionSettings settings) { return new Crypt(MongoCrypts.create(createMongoCryptOptions(settings)), createKeyRetriever(keyVaultClient, settings.getKeyVaultNamespace()), - createKeyManagementService(settings.getKmsProviderSslContextMap(), settings.getProxySettings()), + createKeyManagementService(settings.getKmsProviderSslContextMap()), settings.getKmsProviders(), settings.getKmsProviderPropertySuppliers() ); @@ -79,9 +73,8 @@ private static KeyRetriever createKeyRetriever(final MongoClient keyVaultClient, return new KeyRetriever(keyVaultClient, new MongoNamespace(keyVaultNamespaceString)); } - private static KeyManagementService createKeyManagementService(final Map kmsProviderSslContextMap, - @Nullable final ProxySettings proxySettings) { - return new KeyManagementService(kmsProviderSslContextMap, proxySettings, 10000); + private static KeyManagementService createKeyManagementService(final Map kmsProviderSslContextMap) { + return new KeyManagementService(kmsProviderSslContextMap, 10000); } private Crypts() { diff --git a/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java b/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java index bea86c868d3..c6c04eec0c3 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java +++ b/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java @@ -17,13 +17,11 @@ package com.mongodb.client.internal; import com.mongodb.ServerAddress; -import com.mongodb.connection.ProxySettings; -import com.mongodb.internal.connection.SocksSocket; -import com.mongodb.internal.connection.SslHelper; import com.mongodb.internal.diagnostics.logging.Logger; import com.mongodb.internal.diagnostics.logging.Loggers; -import com.mongodb.lang.Nullable; +import com.mongodb.internal.connection.SslHelper; +import javax.net.SocketFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocket; @@ -37,21 +35,16 @@ import java.nio.ByteBuffer; import java.util.Map; -import static com.mongodb.assertions.Assertions.assertNotNull; import static com.mongodb.assertions.Assertions.notNull; class KeyManagementService { private static final Logger LOGGER = Loggers.getLogger("client"); private final Map kmsProviderSslContextMap; private final int timeoutMillis; - @Nullable - private final ProxySettings proxySettings; - KeyManagementService(final Map kmsProviderSslContextMap, @Nullable final ProxySettings proxySettings, - final int timeoutMillis) { + KeyManagementService(final Map kmsProviderSslContextMap, final int timeoutMillis) { this.kmsProviderSslContextMap = notNull("kmsProviderSslContextMap", kmsProviderSslContextMap); this.timeoutMillis = timeoutMillis; - this.proxySettings = proxySettings; } public InputStream stream(final String kmsProvider, final String host, final ByteBuffer message) throws IOException { @@ -59,29 +52,17 @@ public InputStream stream(final String kmsProvider, final String host, final Byt LOGGER.info("Connecting to KMS server at " + serverAddress); SSLContext sslContext = kmsProviderSslContextMap.get(kmsProvider); - SSLSocketFactory sslSocketFactory = sslContext == null - ? (SSLSocketFactory) SSLSocketFactory.getDefault() : sslContext.getSocketFactory(); - Socket socket = null; + SocketFactory sslSocketFactory = sslContext == null + ? SSLSocketFactory.getDefault() : sslContext.getSocketFactory(); + SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(); + enableHostNameVerification(socket); + try { - final String serverHost = serverAddress.getHost(); - final int serverPort = serverAddress.getPort(); - - if (isProxyEnabled()) { - socket = new SocksSocket(assertNotNull(proxySettings)); - socket.setSoTimeout(timeoutMillis); - socket.connect(InetSocketAddress.createUnresolved(serverHost, serverPort), timeoutMillis); - socket = sslSocketFactory.createSocket(socket, serverHost, serverPort, true); - } else { - socket = sslSocketFactory.createSocket(); - socket.connect(new InetSocketAddress(InetAddress.getByName(serverHost), serverPort), - timeoutMillis); - } - enableHostNameVerification((SSLSocket) socket); + socket.setSoTimeout(timeoutMillis); + socket.connect(new InetSocketAddress(InetAddress.getByName(serverAddress.getHost()), serverAddress.getPort()), timeoutMillis); } catch (IOException e) { - if (socket != null) { - closeSocket(socket); - } + closeSocket(socket); throw e; } @@ -105,10 +86,6 @@ public InputStream stream(final String kmsProvider, final String host, final Byt } } - private boolean isProxyEnabled() { - return proxySettings != null && proxySettings.getHost() != null; - } - private void enableHostNameVerification(final SSLSocket socket) { SSLParameters sslParameters = socket.getSSLParameters(); if (sslParameters == null) { diff --git a/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java index d1fd908289a..fc8a747d1e9 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java @@ -19,12 +19,10 @@ import com.mongodb.MongoClientSettings; import com.mongodb.MongoSocketOpenException; import com.mongodb.MongoTimeoutException; -import com.mongodb.connection.ClusterConnectionMode; import com.mongodb.connection.ClusterDescription; import com.mongodb.connection.ServerDescription; import com.mongodb.event.ClusterDescriptionChangedEvent; import com.mongodb.event.ClusterListener; -import com.mongodb.lang.Nullable; import org.bson.Document; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -39,11 +37,11 @@ import java.util.List; import java.util.Objects; -import java.util.StringJoiner; 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.junit.jupiter.api.Assumptions.assumeTrue; import static org.mockito.Mockito.atLeast; @@ -56,7 +54,6 @@ 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 Boolean SOCKS_AUTH_ENABLED = Boolean.valueOf(System.getProperty("org.mongodb.test.uri.socks.auth.enabled")); - private static final String PROXY_HOST = System.getProperty("org.mongodb.test.uri.proxyHost"); private static final int PROXY_PORT = Integer.parseInt(System.getProperty("org.mongodb.test.uri.proxyPort")); private MongoClient mongoClient; @@ -67,23 +64,27 @@ void tearDown() { } } - static Stream noAuthSettings() { - return Stream.of(buildConnectionString(MONGO_SINGLE_MAPPED_URI_PREFIX, true), - buildConnectionString(MONGO_REPLICA_SET_URI_PREFIX, false)); + 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 invalidAuthSettings() { - return Stream.of(buildConnectionString(MONGO_SINGLE_MAPPED_URI_PREFIX, true, "nonexistentuser", "badauth"), - buildConnectionString(MONGO_REPLICA_SET_URI_PREFIX, false, "nonexistentuser", "badauth")); + 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 validAuthSettings() { - return Stream.of(buildConnectionString(MONGO_SINGLE_MAPPED_URI_PREFIX, true, "username", "p4ssw0rd"), - buildConnectionString(MONGO_REPLICA_SET_URI_PREFIX, false, "username", "p4ssw0rd")); + 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({"noAuthSettings", "invalidAuthSettings"}) + @MethodSource({"noAuthConnectionStrings", "invalidAuthConnectionStrings"}) void shouldConnectWithoutAuth(final ConnectionString connectionString) { assumeFalse(SOCKS_AUTH_ENABLED); mongoClient = MongoClients.create(connectionString); @@ -91,7 +92,7 @@ void shouldConnectWithoutAuth(final ConnectionString connectionString) { } @ParameterizedTest(name = "Should connect without authentication in proxy settings. ConnectionString: {0}") - @MethodSource({"noAuthSettings", "invalidAuthSettings"}) + @MethodSource({"noAuthConnectionStrings", "invalidAuthConnectionStrings"}) void shouldConnectWithoutAuthInProxySettings(final ConnectionString connectionString) { assumeFalse(SOCKS_AUTH_ENABLED); mongoClient = MongoClients.create(buildMongoClientSettings(connectionString)); @@ -99,7 +100,7 @@ void shouldConnectWithoutAuthInProxySettings(final ConnectionString connectionSt } @ParameterizedTest(name = "Should not connect without valid authentication in connection string. ConnectionString: {0}") - @MethodSource({"noAuthSettings", "invalidAuthSettings"}) + @MethodSource({"noAuthConnectionStrings", "invalidAuthConnectionStrings"}) void shouldNotConnectWithoutAuth(final ConnectionString connectionString) { assumeTrue(SOCKS_AUTH_ENABLED); ClusterListener clusterListener = Mockito.mock(ClusterListener.class); @@ -113,7 +114,7 @@ void shouldNotConnectWithoutAuth(final ConnectionString connectionString) { } @ParameterizedTest(name = "Should not connect without valid authentication in proxy settings. ConnectionString: {0}") - @MethodSource({"noAuthSettings", "invalidAuthSettings"}) + @MethodSource({"noAuthConnectionStrings", "invalidAuthConnectionStrings"}) public void shouldNotConnectWithoutAuthInProxySettings(final ConnectionString connectionString) { assumeTrue(SOCKS_AUTH_ENABLED); ClusterListener clusterListener = Mockito.mock(ClusterListener.class); @@ -126,7 +127,7 @@ public void shouldNotConnectWithoutAuthInProxySettings(final ConnectionString co } @ParameterizedTest(name = "Should connect with valid authentication in connection string. ConnectionString: {0}") - @MethodSource("validAuthSettings") + @MethodSource("validAuthConnectionStrings") void shouldConnectWithValidAuth(final ConnectionString connectionString) { assumeTrue(SOCKS_AUTH_ENABLED); mongoClient = MongoClients.create(connectionString); @@ -134,7 +135,7 @@ void shouldConnectWithValidAuth(final ConnectionString connectionString) { } @ParameterizedTest(name = "Should connect with valid authentication in proxy settings. ConnectionString: {0}") - @MethodSource("validAuthSettings") + @MethodSource("validAuthConnectionStrings") public void shouldConnectWithValidAuthInProxySettings(final ConnectionString connectionString) { assumeTrue(SOCKS_AUTH_ENABLED); mongoClient = MongoClients.create(buildMongoClientSettings(connectionString)); @@ -159,56 +160,18 @@ private static void runHelloCommand(final MongoClient mongoClient) { mongoClient.getDatabase("test").runCommand(new Document("hello", 1)); } - private static ConnectionString buildConnectionString(final String uriPrefix, - final boolean directConnection, - @Nullable final String proxyUsername, - @Nullable final String proxyPassword) { - StringJoiner joiner; + private static ConnectionString buildConnectionString(final String uriPrefix, final String uriParameters) { + String format; if (uriPrefix.contains("/?")) { - joiner = new StringJoiner("&", "&", ""); + format = uriPrefix + "&" + uriParameters; } else { - joiner = new StringJoiner("&", "/?", ""); + format = uriPrefix + "/?" + uriParameters; } - - joiner.add("proxyHost=" + PROXY_HOST) - .add("proxyPort=" + PROXY_PORT); - if (proxyPassword != null && proxyUsername != null) { - joiner.add("proxyPassword=" + proxyPassword) - .add("proxyUsername=" + proxyUsername); - } - if (directConnection) { - joiner.add("directConnection=" + true); - } - return new ConnectionString(uriPrefix + joiner); - } - - private static ConnectionString buildConnectionString(final String uriPrefix, final boolean directConnection) { - return buildConnectionString(uriPrefix, directConnection, null, null); + return new ConnectionString(format(format, PROXY_PORT)); } private static MongoClientSettings buildMongoClientSettings(final ConnectionString connectionString) { - return MongoClientSettings.builder().applyToSocketSettings(builder -> builder.applyToProxySettings(proxyBuilder -> { - proxyBuilder.host(connectionString.getProxyHost()); - proxyBuilder.port(connectionString.getProxyPort()); - if (connectionString.getProxyUsername() != null) { - proxyBuilder.username(connectionString.getProxyUsername()); - } - if (connectionString.getProxyPassword() != null) { - proxyBuilder.password(connectionString.getProxyPassword()); - } - })).applyToClusterSettings(builder -> { - if (connectionString.isDirectConnection() != null && connectionString.isDirectConnection()) { - builder.mode(ClusterConnectionMode.SINGLE); - } - }).applyToSslSettings(sslBuilder -> { - if (connectionString.getSslEnabled() != null && connectionString.getSslEnabled()) { - sslBuilder.enabled(connectionString.getSslEnabled()); - } - if (connectionString.getSslInvalidHostnameAllowed() != null && connectionString.getSslInvalidHostnameAllowed()) { - sslBuilder.invalidHostNameAllowed(connectionString.getSslInvalidHostnameAllowed()); - } - }) - .build(); + return MongoClientSettings.builder().applyConnectionString(connectionString).build(); } private static MongoClient createMongoClient(final MongoClientSettings.Builder settingsBuilder, final ClusterListener clusterListener) { @@ -225,9 +188,9 @@ 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 enabled because socks proxy configuration exists"); + return ConditionEvaluationResult.enabled("Test is enabled because socks proxy configuration exists"); } else { - return ConditionEvaluationResult.disabled("Test disabled because socks proxy configuration is missing"); + return ConditionEvaluationResult.disabled("Test is disabled because socks proxy configuration is missing"); } } } From 08d3e304640f384e00cc00051ffede06bebe71c3 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Fri, 1 Sep 2023 14:30:46 -0700 Subject: [PATCH 12/27] Remove KMIP config. JAVA-4347 --- .evergreen/.evg.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index 2e778c7562e..154d7bf5c8b 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -735,18 +735,6 @@ functions: working_dir: src script: | ${PREPARE_SHELL} - export AWS_ACCESS_KEY_ID=${aws_access_key_id} - export AWS_SECRET_ACCESS_KEY=${aws_secret_access_key} - export AWS_DEFAULT_REGION=us-east-1 - . ${DRIVERS_TOOLS}/.evergreen/csfle/set-temp-creds.sh - AWS_ACCESS_KEY_ID=${aws_access_key_id} AWS_SECRET_ACCESS_KEY=${aws_secret_access_key} \ - AWS_TEMP_ACCESS_KEY_ID=$CSFLE_AWS_TEMP_ACCESS_KEY_ID \ - AWS_TEMP_SECRET_ACCESS_KEY=$CSFLE_AWS_TEMP_SECRET_ACCESS_KEY \ - AWS_TEMP_SESSION_TOKEN=$CSFLE_AWS_TEMP_SESSION_TOKEN \ - AZURE_TENANT_ID=${azure_tenant_id} AZURE_CLIENT_ID=${azure_client_id} AZURE_CLIENT_SECRET=${azure_client_secret} \ - GCP_EMAIL=${gcp_email} GCP_PRIVATE_KEY=${gcp_private_key} \ - AZUREKMS_KEY_VAULT_ENDPOINT=${testazurekms_keyvaultendpoint} \ - AZUREKMS_KEY_NAME=${testazurekms_keyname} \ SSL="${SSL}" MONGODB_URI="${MONGODB_URI}" \ JAVA_VERSION="${JAVA_VERSION}" \ .evergreen/run-socks5-tests.sh @@ -1631,7 +1619,6 @@ tasks: - name: test-socks5 tags: [] commands: - - func: start-kms-kmip-server - func: bootstrap mongo-orchestration vars: VERSION: latest @@ -1640,7 +1627,6 @@ tasks: - name: test-socks5-tls tags: [] commands: - - func: start-kms-kmip-server - func: bootstrap mongo-orchestration vars: VERSION: latest From ba111ca4647803d7da2f4e4bf5ee9658fcbb4cb2 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Fri, 1 Sep 2023 17:33:02 -0700 Subject: [PATCH 13/27] Restructuring testing: - Eliminate KMIP server configuration from SOCKS5 tests. - Organize SOCKS5 proxy tests into separate tasks for authenticated and non-authenticated scenarios. - Enhance unit test coverage. JAVA-4347 --- .evergreen/.evg.yml | 26 ++++++----- .evergreen/run-socks5-tests.sh | 46 ++++++++++--------- .../mongodb/client/model/AggregatesTest.java | 32 ++++++------- .../ConnectionStringSpecification.groovy | 35 ++++++++++++++ 4 files changed, 87 insertions(+), 52 deletions(-) diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index 154d7bf5c8b..7003dc20d2e 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -735,6 +735,7 @@ functions: working_dir: src script: | ${PREPARE_SHELL} + SOCKS_AUTH="${SOCKS_AUTH}" \ SSL="${SSL}" MONGODB_URI="${MONGODB_URI}" \ JAVA_VERSION="${JAVA_VERSION}" \ .evergreen/run-socks5-tests.sh @@ -1624,16 +1625,6 @@ tasks: VERSION: latest TOPOLOGY: replica_set - func: run socks5 tests - - name: test-socks5-tls - tags: [] - commands: - - func: bootstrap mongo-orchestration - vars: - VERSION: latest - TOPOLOGY: replica_set - - func: run socks5 tests - vars: - SSL: ssl axes: - id: version display_name: MongoDB Version @@ -1724,6 +1715,17 @@ axes: display_name: NoAuth variables: AUTH: "noauth" + - id: socks_auth + display_name: Socks Proxy Authentication + values: + - id: "auth" + display_name: Auth + variables: + SOCKS_AUTH: "auth" + - id: "noauth" + display_name: NoAuth + variables: + SOCKS_AUTH: "noauth" - id: ssl display_name: SSL values: @@ -2166,8 +2168,8 @@ buildvariants: - name: "csfle-tests-with-mongocryptd" - matrix_name: "socks5-tests" - matrix_spec: { os: "linux", ssl: ["nossl", "ssl"], version: [ "latest" ], topology: ["replicaset"] } - display_name: "Socks5: ${version} ${topology} ${ssl} ${jdk} ${os}" + matrix_spec: { os: "linux", ssl: ["nossl", "ssl"], version: [ "latest" ], topology: ["replicaset"], socks_auth: ["auth", "noauth"] } + display_name: "${socks_auth} SOCKS5 proxy: ${version} ${topology} ${ssl} ${jdk} ${os}" tasks: - name: test-socks5 diff --git a/.evergreen/run-socks5-tests.sh b/.evergreen/run-socks5-tests.sh index 32bdf95a144..b11460b8776 100644 --- a/.evergreen/run-socks5-tests.sh +++ b/.evergreen/run-socks5-tests.sh @@ -4,6 +4,7 @@ set -o xtrace # Write all commands first to stderr set -o errexit # Exit the script with error if any of the commands fail SSL=${SSL:-nossl} +SOCKS_AUTH=${SOCKS_AUTH:-noauth} MONGODB_URI=${MONGODB_URI:-} SOCKS5_SERVER_SCRIPT="$DRIVERS_TOOLS/.evergreen/socks5srv.py" PYTHON_BINARY=${PYTHON_BINARY:-python3} @@ -43,14 +44,31 @@ provision_ssl () { export GRADLE_SSL_VARS="-Pssl.enabled=true -Pssl.keyStoreType=pkcs12 -Pssl.keyStore=`pwd`/client.pkc -Pssl.keyStorePassword=bithere -Pssl.trustStoreType=jks -Pssl.trustStore=`pwd`/mongo-truststore -Pssl.trustStorePassword=changeit" } + +run_socks5_proxy () { +if [ "$SOCKS_AUTH" == "auth" ]; then + "$PYTHON_BINARY" "$SOCKS5_SERVER_SCRIPT" --port 1080 --auth username:p4ssw0rd --map "127.0.0.1:12345 to $FIRST_HOST" & + SOCKS5_SERVER_PID_1=$! + trap "kill $SOCKS5_SERVER_PID_1" EXIT + else + "$PYTHON_BINARY" "$SOCKS5_SERVER_SCRIPT" --port 1080 --map "127.0.0.1:12345 to $FIRST_HOST" & + SOCKS5_SERVER_PID_1=$! + trap "kill $SOCKS5_SERVER_PID_1" EXIT +fi +} + run_socks5_prose_tests () { -local PROXY_PORT=$1 -local AUTH_ENABLED=$2 - echo "Running Socks5 tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connecting to $MONGODB_URI with socks auth enabled $AUTH_ENABLED" +if [ "$SOCKS_AUTH" == "auth" ]; then + local AUTH_ENABLED="true" +else + local AUTH_ENABLED="false" +fi + +echo "Running Socks5 tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connecting to $MONGODB_URI with socks auth enabled: $AUTH_ENABLED" ./gradlew -PjavaVersion=${JAVA_VERSION} -Dorg.mongodb.test.uri=${MONGODB_URI} \ -Dorg.mongodb.test.uri.singleHost=${MONGODB_URI_SINGLEHOST} \ -Dorg.mongodb.test.uri.proxyHost="127.0.0.1" \ - -Dorg.mongodb.test.uri.proxyPort=${PROXY_PORT} \ + -Dorg.mongodb.test.uri.proxyPort="1080" \ -Dorg.mongodb.test.uri.socks.auth.enabled=${AUTH_ENABLED} \ ${GRADLE_SSL_VARS} \ --stacktrace --info --continue \ @@ -64,22 +82,6 @@ local AUTH_ENABLED=$2 # Set up keystore/truststore provision_ssl - -# First, test with Socks5 + authentication required -echo "Running tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connecting to $MONGODB_URI with socks5 auth enabled" ./gradlew -version -"$PYTHON_BINARY" "$SOCKS5_SERVER_SCRIPT" --port 1080 --auth username:p4ssw0rd --map "127.0.0.1:12345 to $FIRST_HOST" & -SOCKS5_SERVER_PID_1=$! -trap "kill $SOCKS5_SERVER_PID_1" EXIT - -run_socks5_prose_tests "1080" "true" - -# Second, test with Socks5 + no authentication -echo "Running tests with Java ${JAVA_VERSION} over $SSL for $TOPOLOGY and connecting to $MONGODB_URI with socks5 auth disabled" -./gradlew -version -"$PYTHON_BINARY" "$SOCKS5_SERVER_SCRIPT" --port 1081 --map "127.0.0.1:12345 to $FIRST_HOST" & -# Set up trap to kill both processes when the script exits -SOCKS5_SERVER_PID_2=$! -trap "kill $SOCKS5_SERVER_PID_1; kill $SOCKS5_SERVER_PID_2" EXIT - -run_socks5_prose_tests "1081" "false" \ No newline at end of file +run_socks5_proxy +run_socks5_prose_tests \ No newline at end of file diff --git a/driver-core/src/test/functional/com/mongodb/client/model/AggregatesTest.java b/driver-core/src/test/functional/com/mongodb/client/model/AggregatesTest.java index 713e4c3f7b6..5ed5ac7cf32 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/AggregatesTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/AggregatesTest.java @@ -24,7 +24,6 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -55,10 +54,8 @@ public class AggregatesTest extends OperationTest { private static Stream groupWithQuantileSource() { return Stream.of( - Arguments.of(percentile("result", "$x", MqlValues.ofNumberArray(0.95), QuantileMethod.approximate()), asList(3.0), - asList(1.0)), - Arguments.of(percentile("result", "$x", MqlValues.ofNumberArray(0.95, 0.3), QuantileMethod.approximate()), asList(3.0, 2.0), - asList(1.0, 1.0)), + Arguments.of(percentile("result", "$x", MqlValues.ofNumberArray(0.95), QuantileMethod.approximate()), asList(3.0), asList(1.0)), + Arguments.of(percentile("result", "$x", MqlValues.ofNumberArray(0.95, 0.3), QuantileMethod.approximate()), asList(3.0, 2.0), asList(1.0, 1.0)), Arguments.of(median("result", "$x", QuantileMethod.approximate()), 2.0d, 1.0d) ); } @@ -167,7 +164,6 @@ public void testUnset() { } @Test - @Disabled public void testGeoNear() { getCollectionHelper().insertDocuments("[\n" + " {\n" @@ -203,18 +199,18 @@ public void testGeoNear() { )); List pipeline = assertPipeline("{\n" - + " $geoNear: {\n" - + " near: { type: 'Point', coordinates: [ -73.99279 , 40.719296 ] },\n" - + " distanceField: 'dist.calculated',\n" - + " minDistance: 0,\n" - + " maxDistance: 2,\n" - + " query: { category: 'Parks' },\n" - + " includeLocs: 'dist.location',\n" - + " spherical: true,\n" - + " key: 'location',\n" - + " distanceMultiplier: 10.0\n" - + " }\n" - + "}", + + " $geoNear: {\n" + + " near: { type: 'Point', coordinates: [ -73.99279 , 40.719296 ] },\n" + + " distanceField: 'dist.calculated',\n" + + " minDistance: 0,\n" + + " maxDistance: 2,\n" + + " query: { category: 'Parks' },\n" + + " includeLocs: 'dist.location',\n" + + " spherical: true,\n" + + " key: 'location',\n" + + " distanceMultiplier: 10.0\n" + + " }\n" + + "}", geoNear( new Point(new Position(-73.99279, 40.719296)), "dist.calculated", diff --git a/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy b/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy index 3d18f8eb6b8..343b473d880 100644 --- a/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy @@ -403,6 +403,41 @@ class ConnectionStringSpecification extends Specification { ' 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/?' From db8d91b654be6172b595ddfbbce9bc25d0191650 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Fri, 1 Sep 2023 18:16:40 -0700 Subject: [PATCH 14/27] Fix checkstyle errors. JAVA-4347 --- .../main/com/mongodb/internal/connection/SocksSocket.java | 1 + .../unit/com/mongodb/ConnectionStringSpecification.groovy | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java index 48a58cb188c..28391f65242 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java @@ -440,6 +440,7 @@ public String getMessage() { } @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, diff --git a/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy b/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy index 343b473d880..536a1e482e4 100644 --- a/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy @@ -433,9 +433,9 @@ class ConnectionStringSpecification extends Specification { 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' + 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'() { From ea1060c8881fd156e20c44898bbef1a041b89340 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Tue, 5 Sep 2023 11:35:30 -0700 Subject: [PATCH 15/27] Update driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/MongoClients.java Co-authored-by: Valentin Kovalenko --- .../main/com/mongodb/reactivestreams/client/MongoClients.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ac249a0d0d1..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 @@ -110,7 +110,7 @@ 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().getHost() != null){ + if (settings.getSocketSettings().getProxySettings().isProxyEnabled()) { throw new MongoClientException("Proxy is not supported for reactive clients"); } if (settings.getStreamFactoryFactory() == null) { From 7b9356558269289c37c6c9db2b7a541e708ca596 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Tue, 5 Sep 2023 11:35:57 -0700 Subject: [PATCH 16/27] Update driver-core/src/main/com/mongodb/connection/ProxySettings.java Co-authored-by: Valentin Kovalenko --- .../src/main/com/mongodb/connection/ProxySettings.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/driver-core/src/main/com/mongodb/connection/ProxySettings.java b/driver-core/src/main/com/mongodb/connection/ProxySettings.java index 21ed69040a9..ed8ed4397de 100644 --- a/driver-core/src/main/com/mongodb/connection/ProxySettings.java +++ b/driver-core/src/main/com/mongodb/connection/ProxySettings.java @@ -39,8 +39,8 @@ *
    * * @see SocketSettings#getProxySettings() - * @see ClientEncryptionSettings#getKeyVaultMongoClientSettings()} - * @see ClientEncryptionSettings#getKeyVaultMongoClientSettings()}. + * @see ClientEncryptionSettings#getKeyVaultMongoClientSettings() + * @see AutoEncryptionSettings#getKeyVaultMongoClientSettings() * @since 4.11 */ @Immutable From e7218a82f77c98b63b3d2657cb71690124becf93 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Tue, 5 Sep 2023 11:36:21 -0700 Subject: [PATCH 17/27] Update driver-core/src/main/com/mongodb/connection/ProxySettings.java Co-authored-by: Valentin Kovalenko --- driver-core/src/main/com/mongodb/connection/ProxySettings.java | 1 + 1 file changed, 1 insertion(+) diff --git a/driver-core/src/main/com/mongodb/connection/ProxySettings.java b/driver-core/src/main/com/mongodb/connection/ProxySettings.java index ed8ed4397de..b5e173cecde 100644 --- a/driver-core/src/main/com/mongodb/connection/ProxySettings.java +++ b/driver-core/src/main/com/mongodb/connection/ProxySettings.java @@ -180,6 +180,7 @@ public ProxySettings.Builder username(final String username) { * @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); From a56d56cbbb768f9d8f3cef02426a6f1a4d295153 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Tue, 5 Sep 2023 11:36:37 -0700 Subject: [PATCH 18/27] Update driver-core/src/main/com/mongodb/connection/ProxySettings.java Co-authored-by: Valentin Kovalenko --- driver-core/src/main/com/mongodb/connection/ProxySettings.java | 1 + 1 file changed, 1 insertion(+) diff --git a/driver-core/src/main/com/mongodb/connection/ProxySettings.java b/driver-core/src/main/com/mongodb/connection/ProxySettings.java index b5e173cecde..ce22e024e30 100644 --- a/driver-core/src/main/com/mongodb/connection/ProxySettings.java +++ b/driver-core/src/main/com/mongodb/connection/ProxySettings.java @@ -158,6 +158,7 @@ public ProxySettings.Builder port(final int port) { * @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); From dcac4f681fc6de3064ff30691995350b99d5a948 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Tue, 5 Sep 2023 11:37:40 -0700 Subject: [PATCH 19/27] Apply suggestions from code review Co-authored-by: Valentin Kovalenko --- .../src/main/com/mongodb/connection/ProxySettings.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/driver-core/src/main/com/mongodb/connection/ProxySettings.java b/driver-core/src/main/com/mongodb/connection/ProxySettings.java index ce22e024e30..01cf317cae6 100644 --- a/driver-core/src/main/com/mongodb/connection/ProxySettings.java +++ b/driver-core/src/main/com/mongodb/connection/ProxySettings.java @@ -119,6 +119,7 @@ public ProxySettings.Builder applySettings(final ProxySettings proxySettings) { * @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); @@ -139,6 +140,7 @@ public ProxySettings.Builder host(final String host) { * @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); @@ -240,7 +242,7 @@ public ProxySettings build() { /** * Gets the SOCKS5 proxy host. * - * @return the proxy host value. + * @return the proxy host value. {@code null} if and only if the {@linkplain #isProxyEnabled() proxy functionality is not enabled}. * @see Builder#host(String) */ @Nullable From 7eb1c11a917fc181ddbf383ed6f522d0a08fd607 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Tue, 5 Sep 2023 12:53:26 -0700 Subject: [PATCH 20/27] - Change javadoc. JAVA-4347 --- .../src/main/com/mongodb/ConnectionString.java | 3 +-- .../com/mongodb/connection/SocketSettings.java | 8 +++++--- .../mongodb/scala/connection/ProxySettings.scala | 12 +++++++----- .../org/mongodb/scala/connection/package.scala | 15 ++++++++------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/driver-core/src/main/com/mongodb/ConnectionString.java b/driver-core/src/main/com/mongodb/ConnectionString.java index e360adbb6f9..b6448635e50 100644 --- a/driver-core/src/main/com/mongodb/ConnectionString.java +++ b/driver-core/src/main/com/mongodb/ConnectionString.java @@ -132,8 +132,7 @@ *
      *
    • {@code proxyHost=string}: The SOCKS5 proxy host to establish a connection through. * It can be provided as a valid IPv4 address, IPv6 address, or a domain name.
    • - *
    • {@code proxyPort=n}: The port number for the SOCKS5 proxy server. Must be a non-negative integer. - * Required if proxyHost is specified.
    • + *
    • {@code proxyPort=n}: The port number for the SOCKS5 proxy server. Must be a non-negative integer.
    • *
    • {@code proxyUsername=string}: Username for authenticating with the proxy server. Required if proxyPassword is specified.
    • *
    • {@code proxyPassword=string}: Password for authenticating with the proxy server. Required if proxyUsername is specified.
    • *
    diff --git a/driver-core/src/main/com/mongodb/connection/SocketSettings.java b/driver-core/src/main/com/mongodb/connection/SocketSettings.java index 409797dc39a..37ad61bbaa5 100644 --- a/driver-core/src/main/com/mongodb/connection/SocketSettings.java +++ b/driver-core/src/main/com/mongodb/connection/SocketSettings.java @@ -140,9 +140,6 @@ public Builder sendBufferSize(final int sendBufferSize) { /** * Applies the {@link ProxySettings.Builder} block and then sets the {@link SocketSettings#proxySettings}. * - *

    - * NOTE: This setting is only applicable to the synchronous variant of MongoClient. - * * @param block the block to apply to the {@link ProxySettings}. * @return this * @see SocketSettings#getProxySettings() @@ -258,16 +255,21 @@ public boolean equals(final Object o) { if (sendBufferSize != that.sendBufferSize) { return false; } + if (proxySettings != that.proxySettings) { + return false; + } return true; } + @Override public int hashCode() { int result = (int) (connectTimeoutMS ^ (connectTimeoutMS >>> 32)); result = 31 * result + (int) (readTimeoutMS ^ (readTimeoutMS >>> 32)); result = 31 * result + receiveBufferSize; result = 31 * result + sendBufferSize; + result = 31 * result + proxySettings.hashCode(); return result; } 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 index e2e501a04b1..3337d742dde 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/connection/ProxySettings.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/connection/ProxySettings.scala @@ -22,12 +22,14 @@ 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. + *

      + *
    • the communication is via `com.mongodb.UnixServerAddress` (Unix domain socket).
    • + *
    • a `StreamFactoryFactory` is `MongoClientSettings.Builder.streamFactoryFactory` configured.
    • + *
    * - * @see [[SocketSettings#getProxySettings]] - * @see [[org.mongodb.scala.AutoEncryptionSettings#getKeyVaultMongoClientSettings]] - * @see [[org.mongodb.scala.ClientEncryptionSettings#getKeyVaultMongoClientSettings]] + * @see [[org.mongodb.scala.connection.SocketSettings]] + * @see [[org.mongodb.scala.AutoEncryptionSettings]] + * @see [[org.mongodb.scala.ClientEncryptionSettings]] * @since 4.11 */ object ProxySettings { 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 bcbfd89ec07..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 @@ -42,16 +42,17 @@ package object connection { type SocketSettings = com.mongodb.connection.SocketSettings /** - * This setting is only applicable when communicating with a MongoDB server or a Key Management Service - * using the synchronous variant of `com.mongodb.MongoClient` or `ClientEncryption`. + * 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. + *
      + *
    • the communication is via `com.mongodb.UnixServerAddress` (Unix domain socket).
    • + *
    • a `StreamFactoryFactory` is `MongoClientSettings.Builder.streamFactoryFactory` configured.
    • + *
    * - * @see [[SocketSettings#getProxySettings]] - * @see [[AutoEncryptionSettings#getProxySettings]] - * @see [[ClientEncryptionSettings#getProxySettings]] + * @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 From 19f2dca7776d0efce93e8966c3fbfac2e778be3f Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Tue, 5 Sep 2023 12:57:45 -0700 Subject: [PATCH 21/27] Add javadoc. JAVA-4347 --- driver-core/src/main/com/mongodb/ConnectionString.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/driver-core/src/main/com/mongodb/ConnectionString.java b/driver-core/src/main/com/mongodb/ConnectionString.java index b6448635e50..c04202edee7 100644 --- a/driver-core/src/main/com/mongodb/ConnectionString.java +++ b/driver-core/src/main/com/mongodb/ConnectionString.java @@ -131,7 +131,8 @@ *

    Proxy Configuration:

    *
      *
    • {@code proxyHost=string}: The SOCKS5 proxy host to establish a connection through. - * It can be provided as a valid IPv4 address, IPv6 address, or a domain name.
    • + * It can be provided as a valid IPv4 address, IPv6 address, or a domain name. Required if either proxyPassword, proxyUsername or + * proxyPort are specified *
    • {@code proxyPort=n}: The port number for the SOCKS5 proxy server. Must be a non-negative integer.
    • *
    • {@code proxyUsername=string}: Username for authenticating with the proxy server. Required if proxyPassword is specified.
    • *
    • {@code proxyPassword=string}: Password for authenticating with the proxy server. Required if proxyUsername is specified.
    • From 735e81e7a1a78bbf240a1eca9dfe00e5d704804c Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Tue, 5 Sep 2023 13:15:38 -0700 Subject: [PATCH 22/27] Remove redundant code. JAVA-4347 --- .../src/main/com/mongodb/internal/connection/SocksSocket.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java index 28391f65242..c9507bce093 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java @@ -49,8 +49,6 @@ *

      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 LENGTH_OF_IPV4 = 4; -// private static final byte LENGTH_OF_IPV6 = 16; private static final byte SOCKS_VERSION = 0x05; private static final byte RESERVED = 0x00; private static final byte PORT_SIZE = 2; @@ -237,7 +235,7 @@ private void authenticate(final SocksAuthenticationMethod authenticationMethod, byte authStatus = authResult[1]; if (authStatus != AUTHENTICATION_SUCCEEDED_STATUS) { - throw new ConnectException("Authentication failed. Server returned status: " + authStatus); + throw new ConnectException("Authentication failed. Proxy server returned status: " + authStatus); } } } From 34a26ce840a29e218587a2b7d403fe2103a64d04 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Tue, 5 Sep 2023 13:32:14 -0700 Subject: [PATCH 23/27] Add @EnabledIf annotation for tests to remove skipped tests from a result report. JAVA-4347 --- .../com/mongodb/connection/ProxySettings.java | 1 + .../mongodb/connection/SocketSettings.java | 7 +----- .../com/mongodb/client/Socks5ProseTest.java | 24 +++++++++++-------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/driver-core/src/main/com/mongodb/connection/ProxySettings.java b/driver-core/src/main/com/mongodb/connection/ProxySettings.java index 01cf317cae6..1a4c793f875 100644 --- a/driver-core/src/main/com/mongodb/connection/ProxySettings.java +++ b/driver-core/src/main/com/mongodb/connection/ProxySettings.java @@ -16,6 +16,7 @@ package com.mongodb.connection; +import com.mongodb.AutoEncryptionSettings; import com.mongodb.ClientEncryptionSettings; import com.mongodb.ConnectionString; import com.mongodb.annotations.Immutable; diff --git a/driver-core/src/main/com/mongodb/connection/SocketSettings.java b/driver-core/src/main/com/mongodb/connection/SocketSettings.java index 37ad61bbaa5..7a63790cb66 100644 --- a/driver-core/src/main/com/mongodb/connection/SocketSettings.java +++ b/driver-core/src/main/com/mongodb/connection/SocketSettings.java @@ -255,14 +255,9 @@ public boolean equals(final Object o) { if (sendBufferSize != that.sendBufferSize) { return false; } - if (proxySettings != that.proxySettings) { - return false; - } - - return true; + return proxySettings.equals(that.proxySettings); } - @Override public int hashCode() { int result = (int) (connectTimeoutMS ^ (connectTimeoutMS >>> 32)); diff --git a/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java index fc8a747d1e9..f2459d571b9 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java @@ -26,6 +26,8 @@ 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; @@ -43,7 +45,6 @@ import static java.lang.String.format; import static org.junit.jupiter.api.Assumptions.assumeFalse; -import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.mockito.Mockito.atLeast; /** @@ -53,7 +54,6 @@ 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 Boolean SOCKS_AUTH_ENABLED = Boolean.valueOf(System.getProperty("org.mongodb.test.uri.socks.auth.enabled")); private static final int PROXY_PORT = Integer.parseInt(System.getProperty("org.mongodb.test.uri.proxyPort")); private MongoClient mongoClient; @@ -85,24 +85,24 @@ static Stream validAuthConnectionStrings() { @ParameterizedTest(name = "Should connect without authentication in connection string. ConnectionString: {0}") @MethodSource({"noAuthConnectionStrings", "invalidAuthConnectionStrings"}) + @DisabledIf("isAuthEnabled") void shouldConnectWithoutAuth(final ConnectionString connectionString) { - assumeFalse(SOCKS_AUTH_ENABLED); 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) { - assumeFalse(SOCKS_AUTH_ENABLED); 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) { - assumeTrue(SOCKS_AUTH_ENABLED); ClusterListener clusterListener = Mockito.mock(ClusterListener.class); ArgumentCaptor captor = ArgumentCaptor.forClass(ClusterDescriptionChangedEvent.class); @@ -115,8 +115,8 @@ void shouldNotConnectWithoutAuth(final ConnectionString connectionString) { @ParameterizedTest(name = "Should not connect without valid authentication in proxy settings. ConnectionString: {0}") @MethodSource({"noAuthConnectionStrings", "invalidAuthConnectionStrings"}) - public void shouldNotConnectWithoutAuthInProxySettings(final ConnectionString connectionString) { - assumeTrue(SOCKS_AUTH_ENABLED); + @EnabledIf("isAuthEnabled") + void shouldNotConnectWithoutAuthInProxySettings(final ConnectionString connectionString) { ClusterListener clusterListener = Mockito.mock(ClusterListener.class); ArgumentCaptor captor = ArgumentCaptor.forClass(ClusterDescriptionChangedEvent.class); @@ -128,16 +128,16 @@ public void shouldNotConnectWithoutAuthInProxySettings(final ConnectionString co @ParameterizedTest(name = "Should connect with valid authentication in connection string. ConnectionString: {0}") @MethodSource("validAuthConnectionStrings") + @EnabledIf("isAuthEnabled") void shouldConnectWithValidAuth(final ConnectionString connectionString) { - assumeTrue(SOCKS_AUTH_ENABLED); mongoClient = MongoClients.create(connectionString); runHelloCommand(mongoClient); } @ParameterizedTest(name = "Should connect with valid authentication in proxy settings. ConnectionString: {0}") @MethodSource("validAuthConnectionStrings") - public void shouldConnectWithValidAuthInProxySettings(final ConnectionString connectionString) { - assumeTrue(SOCKS_AUTH_ENABLED); + @EnabledIf("isAuthEnabled") + void shouldConnectWithValidAuthInProxySettings(final ConnectionString connectionString) { mongoClient = MongoClients.create(buildMongoClientSettings(connectionString)); runHelloCommand(mongoClient); } @@ -184,6 +184,10 @@ private static MongoClient createMongoClient(final MongoClientSettings.Builder s .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) { From ede85deb29d21309d79730808f705d7aab4a3a07 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Wed, 6 Sep 2023 11:40:44 -0700 Subject: [PATCH 24/27] Apply suggestions from code review Co-authored-by: Valentin Kovalenko --- .../src/main/com/mongodb/internal/connection/SocksSocket.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java index c9507bce093..8cbd2ab2d53 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java @@ -89,7 +89,7 @@ public void connect(final SocketAddress endpoint, final int timeoutMs) throws IO assertTrue(unresolvedAddress.isUnresolved()); this.remoteAddress = unresolvedAddress; - InetSocketAddress proxyAddress = new InetSocketAddress(proxySettings.getHost(), proxySettings.getPort()); + InetSocketAddress proxyAddress = new InetSocketAddress(assertNotNull(proxySettings.getHost()), proxySettings.getPort()); if (socket != null) { socket.connect(proxyAddress, remainingMillis(timeout)); } else { @@ -432,7 +432,7 @@ public int getReplyNumber() { return replyNumber; } - public String getMessage() { + String getMessage() { return message; } } From 840d12bb71506e9afe4eb12a32911c349b5df605 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 6 Sep 2023 11:45:04 -0700 Subject: [PATCH 25/27] Remove redundant code. JAVA-4347 --- .evergreen/.evg.yml | 2 +- .../internal/connection/SocksSocket.java | 52 ++++++++++--------- .../com/mongodb/client/Socks5ProseTest.java | 10 ++-- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index 7003dc20d2e..3ec7719c94d 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -2169,7 +2169,7 @@ buildvariants: - matrix_name: "socks5-tests" matrix_spec: { os: "linux", ssl: ["nossl", "ssl"], version: [ "latest" ], topology: ["replicaset"], socks_auth: ["auth", "noauth"] } - display_name: "${socks_auth} SOCKS5 proxy: ${version} ${topology} ${ssl} ${jdk} ${os}" + display_name: "SOCKS5 proxy ${socks_auth} : ${version} ${topology} ${ssl} ${jdk} ${os}" tasks: - name: test-socks5 diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java index 8cbd2ab2d53..b4f9069fd33 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java @@ -51,7 +51,7 @@ public final class SocksSocket extends Socket { private static final byte SOCKS_VERSION = 0x05; private static final byte RESERVED = 0x00; - private static final byte PORT_SIZE = 2; + 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; @@ -95,7 +95,7 @@ public void connect(final SocketAddress endpoint, final int timeoutMs) throws IO } else { super.connect(proxyAddress, remainingMillis(timeout)); } - SocksAuthenticationMethod authenticationMethod = performHandshake(timeout); + SocksAuthenticationMethod authenticationMethod = performNegotiation(timeout); authenticate(authenticationMethod, timeout); sendConnect(timeout); } catch (SocketException socketException) { @@ -155,8 +155,8 @@ private void sendConnect(final Timeout timeout) throws IOException { } private static void addPort(final byte[] bufferSent, final int index, final int port) { - bufferSent[index] = (byte) ((port & 0xff00) >> 8); - bufferSent[index + 1] = (byte) (port & 0xff); + bufferSent[index] = (byte) (port >> 8); + bufferSent[index + 1] = (byte) port; } private static byte[] createByteArrayFromIpAddress(final String host) throws SocketException { @@ -196,13 +196,13 @@ private void checkServerReply(final Timeout timeout) throws IOException { switch (AddressType.of(data[3])) { case DOMAIN_NAME: byte hostNameLength = readSocksReply(1, timeout)[0]; - readSocksReply(hostNameLength + PORT_SIZE, timeout); + readSocksReply(hostNameLength + PORT_LENGTH, timeout); break; case IP_V4: - readSocksReply(IP_V4.getLength() + PORT_SIZE, timeout); + readSocksReply(IP_V4.getLength() + PORT_LENGTH, timeout); break; case IP_V6: - readSocksReply(IP_V6.getLength() + PORT_SIZE, timeout); + readSocksReply(IP_V6.getLength() + PORT_LENGTH, timeout); break; default: throw fail(); @@ -240,7 +240,7 @@ private void authenticate(final SocksAuthenticationMethod authenticationMethod, } } - private SocksAuthenticationMethod performHandshake(final Timeout timeout) throws IOException { + private SocksAuthenticationMethod performNegotiation(final Timeout timeout) throws IOException { SocksAuthenticationMethod[] authenticationMethods = getSocksAuthenticationMethods(); int methodsCount = authenticationMethods.length; @@ -330,6 +330,12 @@ private byte[] readSocksReply(final int length, final Timeout timeout) throws IO return data; } + private static void validateDomainLength(final int hostLength) throws ConnectException { + if(hostLength > 255){ + throw new ConnectException("Domain name length in bytes exceeds the maximum allowed length of 255 in SOCKS5"); + } + } + enum SocksCommand { CONNECT(0x01); @@ -401,13 +407,13 @@ byte getAddressTypeNumber() { enum ServerReply { REPLY_SUCCEEDED(0x00, "Succeeded"), GENERAL_FAILURE(0x01, "General SOCKS5 server failure"), - NOT_ALLOWED(0x02, "Proxy server general failure"), - NET_UNREACHABLE(0x03, "Connection not allowed by ruleset"), - HOST_UNREACHABLE(0x04, "Network is unreachable"), - CONN_REFUSED(0x05, "Host is unreachable"), - TTL_EXPIRED(0x06, "Connection has been refused"), - CMD_NOT_SUPPORTED(0x07, "TTL expired"), - ADDR_TYPE_NOT_SUP(0x08, "Address type not supported"); + 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; @@ -420,7 +426,7 @@ enum ServerReply { static ServerReply of(final byte byteStatus) throws ConnectException { int status = Byte.toUnsignedInt(byteStatus); for (ServerReply serverReply : ServerReply.values()) { - if (status == serverReply.getReplyNumber()) { + if (status == serverReply.replyNumber) { return serverReply; } } @@ -428,11 +434,7 @@ static ServerReply of(final byte byteStatus) throws ConnectException { throw new ConnectException("Unknown reply field. Reply field: " + status); } - public int getReplyNumber() { - return replyNumber; - } - - String getMessage() { + public String getMessage() { return message; } } @@ -604,7 +606,7 @@ public boolean getOOBInline() throws SocketException { } @Override - public synchronized void setSendBufferSize(final int size) throws SocketException { + public void setSendBufferSize(final int size) throws SocketException { if (socket != null) { socket.setSendBufferSize(size); } else { @@ -613,7 +615,7 @@ public synchronized void setSendBufferSize(final int size) throws SocketExceptio } @Override - public synchronized int getSendBufferSize() throws SocketException { + public int getSendBufferSize() throws SocketException { if (socket != null) { return socket.getSendBufferSize(); } else { @@ -622,7 +624,7 @@ public synchronized int getSendBufferSize() throws SocketException { } @Override - public synchronized void setReceiveBufferSize(final int size) throws SocketException { + public void setReceiveBufferSize(final int size) throws SocketException { if (socket != null) { socket.setReceiveBufferSize(size); } else { @@ -631,7 +633,7 @@ public synchronized void setReceiveBufferSize(final int size) throws SocketExcep } @Override - public synchronized int getReceiveBufferSize() throws SocketException { + public int getReceiveBufferSize() throws SocketException { if (socket != null) { return socket.getReceiveBufferSize(); } else { diff --git a/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java index f2459d571b9..7beb11ee5f9 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/Socks5ProseTest.java @@ -104,13 +104,12 @@ void shouldConnectWithoutAuthInProxySettings(final ConnectionString connectionSt @EnabledIf("isAuthEnabled") void shouldNotConnectWithoutAuth(final ConnectionString connectionString) { ClusterListener clusterListener = Mockito.mock(ClusterListener.class); - ArgumentCaptor captor = ArgumentCaptor.forClass(ClusterDescriptionChangedEvent.class); mongoClient = createMongoClient(MongoClientSettings.builder() .applyConnectionString(connectionString), clusterListener); Assertions.assertThrows(MongoTimeoutException.class, () -> runHelloCommand(mongoClient)); - assertSocksAuthenticationIssue(clusterListener, captor); + assertSocksAuthenticationIssue(clusterListener); } @ParameterizedTest(name = "Should not connect without valid authentication in proxy settings. ConnectionString: {0}") @@ -118,12 +117,11 @@ void shouldNotConnectWithoutAuth(final ConnectionString connectionString) { @EnabledIf("isAuthEnabled") void shouldNotConnectWithoutAuthInProxySettings(final ConnectionString connectionString) { ClusterListener clusterListener = Mockito.mock(ClusterListener.class); - ArgumentCaptor captor = ArgumentCaptor.forClass(ClusterDescriptionChangedEvent.class); mongoClient = createMongoClient(MongoClientSettings.builder(buildMongoClientSettings(connectionString)), clusterListener); Assertions.assertThrows(MongoTimeoutException.class, () -> runHelloCommand(mongoClient)); - assertSocksAuthenticationIssue(clusterListener, captor); + assertSocksAuthenticationIssue(clusterListener); } @ParameterizedTest(name = "Should connect with valid authentication in connection string. ConnectionString: {0}") @@ -142,8 +140,8 @@ void shouldConnectWithValidAuthInProxySettings(final ConnectionString connection runHelloCommand(mongoClient); } - private static void assertSocksAuthenticationIssue(final ClusterListener clusterListener, - final ArgumentCaptor captor) { + 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) From 7d461a00928b4be28bed48f7e6795128b068e26d Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 6 Sep 2023 14:41:50 -0700 Subject: [PATCH 26/27] Add size check in domain regex. JAVA-4347 --- .../mongodb/internal/connection/DomainNameUtils.java | 2 +- .../com/mongodb/internal/connection/SocksSocket.java | 8 +------- .../internal/connection/DomainNameUtilsTest.java | 10 +++++++++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java b/driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java index f45230146a7..a1f0938e104 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java +++ b/driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java @@ -22,7 +22,7 @@ */ public class DomainNameUtils { private static final Pattern DOMAIN_PATTERN = - Pattern.compile("^(([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}|localhost)$"); + 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/SocksSocket.java b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java index b4f9069fd33..6d19d7f5a5c 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java @@ -113,7 +113,7 @@ public void connect(final SocketAddress endpoint, final int timeoutMs) throws IO private void sendConnect(final Timeout timeout) throws IOException { final String host = remoteAddress.getHostName(); final int port = remoteAddress.getPort(); - final byte[] bytesOfHost = host.getBytes(StandardCharsets.UTF_8); + final byte[] bytesOfHost = host.getBytes(StandardCharsets.US_ASCII); final int hostLength = bytesOfHost.length; AddressType addressType; @@ -330,12 +330,6 @@ private byte[] readSocksReply(final int length, final Timeout timeout) throws IO return data; } - private static void validateDomainLength(final int hostLength) throws ConnectException { - if(hostLength > 255){ - throw new ConnectException("Domain name length in bytes exceeds the maximum allowed length of 255 in SOCKS5"); - } - } - enum SocksCommand { CONNECT(0x01); diff --git a/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java b/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java index 36fbb638db7..293dc762bce 100644 --- a/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java +++ b/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java @@ -38,6 +38,9 @@ class DomainNameUtilsTest { "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)); @@ -55,9 +58,14 @@ void shouldReturnTrueWithValidHostName(final String hostname) { "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" + "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)); From 42aca98c0fba8dddbfb7dcd17ba411f30cffaf75 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 6 Sep 2023 14:53:32 -0700 Subject: [PATCH 27/27] Address checkstyle warnings. JAVA-4347 --- .../internal/connection/DomainNameUtilsTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java b/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java index 293dc762bce..cc987cacf62 100644 --- a/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java +++ b/driver-core/src/test/functional/com/mongodb/internal/connection/DomainNameUtilsTest.java @@ -38,9 +38,9 @@ class DomainNameUtilsTest { "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 + "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)); @@ -63,9 +63,9 @@ void shouldReturnTrueWithValidHostName(final String hostname) { "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 + "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));