diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
index dd60a2487fb..babeffffdcf 100644
--- a/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
+++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
@@ -1184,9 +1184,9 @@ public enum DefaultDriverOption implements DriverOption {
* system_views.clients} on Cassandra 4.1+) so operators can inspect driver settings while
* investigating incidents. It describes the effective configuration of the driver's default
* execution profile (connection/socket settings, timeouts, retry/reconnection/
- * speculative-execution/load-balancing policies, connection pooling, query defaults, and TLS).
- * Only the control connection sends it, since it describes the whole session. When {@code false},
- * {@code DRIVER_CONFIG} is not sent.
+ * speculative-execution/load-balancing policies, connection pooling, and query defaults), plus
+ * the effective TLS state of the control connection carrying it. Only the control connection
+ * sends it. When {@code false}, {@code DRIVER_CONFIG} is not sent.
*
*
This option governs {@code DRIVER_CONFIG} only. The {@code SESSION_ID} startup option, which
* lets the server group all of a session's connections, is an innate driver behavior: it is sent
diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java
index dd7630a6530..d7e9bd06c1d 100644
--- a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java
+++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java
@@ -35,6 +35,7 @@
import com.datastax.oss.driver.api.core.metadata.EndPoint;
import com.datastax.oss.driver.api.core.type.codec.TypeCodecs;
import com.datastax.oss.driver.internal.core.DefaultProtocolFeature;
+import com.datastax.oss.driver.internal.core.context.DriverConfigReporter.TlsInfo;
import com.datastax.oss.driver.internal.core.context.InternalDriverContext;
import com.datastax.oss.driver.internal.core.protocol.BytesToSegmentDecoder;
import com.datastax.oss.driver.internal.core.protocol.FrameDecoder;
@@ -62,8 +63,10 @@
import com.datastax.oss.protocol.internal.response.Supported;
import com.datastax.oss.protocol.internal.response.result.Rows;
import com.datastax.oss.protocol.internal.response.result.SetKeyspace;
+import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
+import io.netty.handler.ssl.SslHandler;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
@@ -192,12 +195,14 @@ Message getRequest() {
case STARTUP:
Map startupOptions = new HashMap<>(context.getStartupOptions());
featureStore.populateStartupOptions(startupOptions);
- // The DRIVER_CONFIG blob describes the whole session, so only the control connection
- // carries it (options.reportConfig); the other connections are correlated to it by the
- // SESSION_ID that every connection already carries from context.getStartupOptions().
- // No-op when driver config reporting is disabled.
+ // Most of DRIVER_CONFIG describes the whole session; its TLS group describes the control
+ // connection carrying it. Other connections are correlated to it by the SESSION_ID that
+ // every connection already carries from context.getStartupOptions(). No-op when driver
+ // config reporting is disabled.
if (options.reportConfig) {
- context.getDriverConfigReporter().populateControlConnectionOptions(startupOptions);
+ context
+ .getDriverConfigReporter()
+ .populateControlConnectionOptions(startupOptions, currentTlsInfo());
}
return request = new Startup(startupOptions);
case GET_CLUSTER_NAME:
@@ -213,6 +218,21 @@ Message getRequest() {
}
}
+ private TlsInfo currentTlsInfo() {
+ try {
+ return tlsInfo(channel);
+ } catch (RuntimeException e) {
+ // Configuration reporting is best-effort and must never prevent a connection. TLS presence
+ // is still assumed because the failure came while inspecting its handler, but the schema
+ // permits hostname-verification to be omitted when unknown.
+ LOG.warn(
+ "[{}] Could not inspect hostname verification on the active SSL engine; omitting it",
+ logPrefix,
+ e);
+ return TlsInfo.enabledWithUnknownHostnameVerification();
+ }
+ }
+
@Override
void send() {
stepNumber++;
@@ -416,6 +436,21 @@ public String toString() {
}
}
+ static TlsInfo tlsInfo(Channel channel) {
+ // SslHandlerFactory always returns SslHandler, and this reads the pipeline after
+ // NettyOptions.afterChannelInitialized() has had a chance to add, replace, remove, or
+ // reconfigure it. A hook that implements encryption with a handler unrelated to SslHandler
+ // cannot be identified generically and is therefore reported as TLS-disabled.
+ SslHandler sslHandler = channel.pipeline().get(SslHandler.class);
+ if (sslHandler == null) {
+ return TlsInfo.disabled();
+ }
+ String endpointIdentificationAlgorithm =
+ sslHandler.engine().getSSLParameters().getEndpointIdentificationAlgorithm();
+ return TlsInfo.enabled(
+ endpointIdentificationAlgorithm != null && !endpointIdentificationAlgorithm.isEmpty());
+ }
+
/**
* Conditionally rebuilds pipeline.
*
diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java
index f48d686573c..d90a2e7fea0 100644
--- a/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java
+++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java
@@ -26,8 +26,6 @@
import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy;
import com.datastax.oss.driver.api.core.retry.RetryPolicy;
import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy;
-import com.datastax.oss.driver.api.core.ssl.ProgrammaticSslEngineFactory;
-import com.datastax.oss.driver.api.core.ssl.SslEngineFactory;
import com.datastax.oss.driver.api.core.time.TimestampGenerator;
import com.datastax.oss.driver.internal.core.channel.ChannelFactory;
import com.datastax.oss.driver.internal.core.connection.ConstantReconnectionPolicy;
@@ -39,10 +37,6 @@
import com.datastax.oss.driver.internal.core.retry.DefaultRetryPolicy;
import com.datastax.oss.driver.internal.core.specex.ConstantSpeculativeExecutionPolicy;
import com.datastax.oss.driver.internal.core.specex.NoSpeculativeExecutionPolicy;
-import com.datastax.oss.driver.internal.core.ssl.DefaultSslEngineFactory;
-import com.datastax.oss.driver.internal.core.ssl.JdkSslHandlerFactory;
-import com.datastax.oss.driver.internal.core.ssl.SniSslEngineFactory;
-import com.datastax.oss.driver.internal.core.ssl.SslHandlerFactory;
import com.datastax.oss.driver.internal.core.time.AtomicTimestampGenerator;
import com.datastax.oss.driver.internal.core.time.ServerSideTimestampGenerator;
import com.datastax.oss.driver.internal.core.time.ThreadLocalTimestampGenerator;
@@ -50,6 +44,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
@@ -75,8 +70,8 @@
* {@code connection.reconnection.policy}, {@code query.speculative-execution.policy}, {@code
* query.load-balancing.policy.adaptive-ordering}, the first term of {@code
* fallback-to-non-preferred-nodes}, both {@code node-preference} groups, and {@code
- * connection.tls.hostname-verification} off the active handler factory rather than the configured
- * engine factory.
+ * connection.tls.hostname-verification} from an immutable snapshot of the active control-channel
+ * engine rather than the configured engine factory.
*
* Anything added here owes the same question: find what consumes the option, and if it stores
* the value rather than re-reading it, expose an accessor and read that. Every field that got this
@@ -104,13 +99,10 @@
* basic.request.serial-consistency} outside the schema's two serial levels and the like are omitted
* rather than emitted as a value the schema rejects. Two optional booleans are omitted for a third
* reason — the answer is genuinely unknown, which is the only thing the schema lets their absence
- * mean: {@code connection.tls.hostname-verification} when the SSL handler or engine factory in
- * force is not one this class recognizes, and {@code query.defaults.client-timestamps} when the
- * timestamp generator is not (see {@link #hostnameValidation} and {@link #clientTimestamps}).
- * Guessing a boolean there would describe a security control, or a write-timestamp source, that may
- * well be the opposite — which is also why neither is asked of the SPI itself: an accessor on
- * {@link SslEngineFactory} or {@link TimestampGenerator} would have needed a default, and a default
- * answer is exactly the guess being avoided.
+ * mean: {@code connection.tls.hostname-verification} when the active SSL engine could not expose
+ * it, and {@code query.defaults.client-timestamps} when the timestamp generator is not one this
+ * class recognizes (see {@link #tls} and {@link #clientTimestamps}). Guessing a boolean there would
+ * describe a security control, or a write-timestamp source, that may well be the opposite.
*
*
A new field owes three checks, each of which this class has already got wrong once and
* each of which is cheap to run before review does it for you:
@@ -189,8 +181,8 @@
* cross-driver schema doesn't define; this is a known gap, not an oversight.
*
*
Thread safety: this class is safe to use as shipped, and holds no mutable state. Note
- * that {@code buildJson()} runs on every control-connection (re)initialization, and may be called
- * concurrently with a reconnect racing a fresh session start.
+ * that {@code buildJson(DriverConfigReporter.TlsInfo)} runs on every control-connection
+ * (re)initialization, and may be called concurrently with a reconnect racing a fresh session start.
*/
@ThreadSafe
public class DefaultDriverConfigReporter implements DriverConfigReporter {
@@ -231,7 +223,8 @@ public DefaultDriverConfigReporter(InternalDriverContext context) {
}
@Override
- public void populateControlConnectionOptions(Map startupOptions) {
+ public void populateControlConnectionOptions(
+ @NonNull Map startupOptions, @NonNull TlsInfo tlsInfo) {
// Configuration reporting is a best-effort diagnostic aid: it runs on the connection
// initialization path, so any failure here (a bad config read, a misbehaving policy while
// introspecting, a serialization error) must be swallowed rather than allowed to break the
@@ -247,7 +240,7 @@ public void populateControlConnectionOptions(Map startupOptions)
if (!isEnabled()) {
return;
}
- String json = buildJson();
+ String json = buildJson(tlsInfo);
if (json == null) {
return;
}
@@ -288,22 +281,20 @@ private boolean isEnabled() {
* class's to enforce: a future change to session bootstrap that dropped one of those from the
* eager list would quietly reintroduce that.
*
- * The configured SSL engine factory is deliberately not among them: {@link #tls()}
- * reads the engine factory held by the {@code JdkSslHandlerFactory} in force rather than the one
- * behind {@code getSslEngineFactory()}. Those can differ — a context that overrides {@code
- * buildSslHandlerFactory()} may wrap an engine factory of its own — and going through the context
- * would both describe an engine nothing on the connection path uses and risk being the first
- * caller to resolve it, which for the built-in factory means reading keystore/truststore files on
- * a Netty event-loop thread (and failing the whole report if that throws).
+ *
The configured SSL engine factory is deliberately not among them: {@link
+ * #tls(DriverConfigReporter.TlsInfo)} reads the immutable snapshot captured from the active
+ * control-channel handler. Resolving the configured factory here could describe an engine the
+ * connection does not use and, for the built-in factory, read keystore/truststore files on a
+ * Netty event-loop thread.
*
* @return the report, or {@code null} if it could not be serialized — in which case {@code
* DRIVER_CONFIG} is skipped rather than the connection failed.
*/
@Nullable
- String buildJson() {
+ String buildJson(TlsInfo tlsInfo) {
ObjectNode root = OBJECT_MAPPER.createObjectNode();
root.put("version", SCHEMA_VERSION);
- populateConfig(root, context.getConfig().getDefaultProfile());
+ populateConfig(root, context.getConfig().getDefaultProfile(), tlsInfo);
try {
return OBJECT_MAPPER.writeValueAsString(root);
} catch (JsonProcessingException e) {
@@ -318,14 +309,14 @@ String buildJson() {
* plus the context's policies. Each group follows the cross-driver schema; a key the Java driver
* has no equivalent for is omitted rather than reported as {@code null}.
*/
- private void populateConfig(ObjectNode root, DriverExecutionProfile config) {
+ private void populateConfig(ObjectNode root, DriverExecutionProfile config, TlsInfo tlsInfo) {
// Resolved once and shared: the load balancing policy decides both its own group and the
// node-location preferences reported under two different parents, and resolving it twice would
// mean a second SPI lookup on the Netty event-loop thread that is building STARTUP.
LoadBalancingPolicy loadBalancingPolicy =
context.getLoadBalancingPolicy(DriverExecutionProfile.DEFAULT_NAME);
NodeLocation nodeLocation = nodeLocation(config, loadBalancingPolicy);
- root.set("connection", connection(config, nodeLocation));
+ root.set("connection", connection(config, nodeLocation, tlsInfo));
root.set("control-plane", controlPlane(config));
root.set("query", query(config, loadBalancingPolicy, nodeLocation));
}
@@ -335,7 +326,7 @@ private void populateConfig(ObjectNode root, DriverExecutionProfile config) {
* top of it, how it is re-established, and which part of the cluster gets one at all.
*/
private ObjectNode connection(
- DriverExecutionProfile config, @Nullable NodeLocation nodeLocation) {
+ DriverExecutionProfile config, @Nullable NodeLocation nodeLocation, TlsInfo tlsInfo) {
ObjectNode n = connectionTimeouts(config);
n.set("socket", socket(config));
ObjectNode reconnection = OBJECT_MAPPER.createObjectNode();
@@ -343,7 +334,7 @@ private ObjectNode connection(
n.set("reconnection", reconnection);
// Optional, and absent rather than false when off: presence of the group is what says TLS is
// enabled, since the schema dropped the boolean that used to carry it.
- ObjectNode tls = tls();
+ ObjectNode tls = tls(tlsInfo);
if (tls != null) {
n.set("tls", tls);
}
@@ -1059,9 +1050,9 @@ private ObjectNode queryDefaults(DriverExecutionProfile config) {
* which both of them extend, is package-private, so nothing outside its own package can inherit
* its behavior without going through one of these two.
*
- *
{@code instanceof}, not the exact-class checks the policy branches use, for the same reason
- * as in {@link #hostnameValidation}: this reads a property the generator has rather than deciding
- * which built-in is in force, and a subclass inherits the {@code next()} that supplies it.
+ *
{@code instanceof}, not the exact-class checks the policy branches use: this reads a
+ * property the generator has rather than deciding which built-in is in force, and a subclass
+ * inherits the {@code next()} that supplies it.
*/
private static Optional clientTimestamps(TimestampGenerator generator) {
if (generator instanceof AtomicTimestampGenerator
@@ -1078,73 +1069,18 @@ private static Optional clientTimestamps(TimestampGenerator generator)
* so presence of the group is what reports that it is on.
*/
@Nullable
- private ObjectNode tls() {
- // TLS is on exactly when the channel pipeline gets an SSL handler, which ChannelFactory decides
- // from the low-level SslHandlerFactory. Deliberately not getSslEngineFactory(): that is only
- // the public JDK-based path that DefaultDriverContext.buildSslHandlerFactory() wraps, and an
- // override of that method (the documented expert extension point, e.g. Netty's native OpenSSL)
- // supplies a handler factory with no engine factory at all — a session that is encrypted all
- // the same.
- Optional handlerFactory = context.getSslHandlerFactory();
- if (!handlerFactory.isPresent()) {
+ private ObjectNode tls(TlsInfo tlsInfo) {
+ // The snapshot is captured from the active pipeline immediately before STARTUP, after the
+ // per-channel customization hook has run. Deliberately not getSslEngineFactory(): that is
+ // configuration intent and may describe an engine this connection does not use.
+ if (!tlsInfo.isEnabled()) {
return null;
}
ObjectNode n = OBJECT_MAPPER.createObjectNode();
- // Host name validation, on the other hand, is a property of the JDK SSLEngine that the engine
- // factory configures, so it can only be read on the JDK path — when the handler factory in
- // force is the JdkSslHandlerFactory that buildSslHandlerFactory() wraps an engine factory in —
- // and read off that handler rather than through the context (see #buildJson for why the two can
- // disagree, and why resolving the context's is worse). Anything else (a native-OpenSSL handler,
- // a bespoke one) leaves it unknown, and the schema's field is optional precisely so that
- // unknown can be said by omission: reporting false would claim a session is not checking host
- // names when it may well be. Exact-class check, like the policy branches above:
- // JdkSslHandlerFactory is not final, and a subclass need not use the engine it was given.
- //
- // Note this is the factory's own state, not the SSL_HOSTNAME_VALIDATION config option: that
- // option only governs the built-in DefaultSslEngineFactory. A factory supplied via
- // SessionBuilder.withSslContext(...) (ProgrammaticSslEngineFactory) validates only if
- // explicitly asked to (default off) regardless of that option, so reading the option here would
- // falsely report validation as on when it isn't.
- SslHandlerFactory factory = handlerFactory.get();
- if (factory.getClass() == JdkSslHandlerFactory.class) {
- SslEngineFactory engineFactory = ((JdkSslHandlerFactory) factory).getSslEngineFactory();
- hostnameValidation(engineFactory).ifPresent(v -> n.put("hostname-verification", v));
- }
+ tlsInfo.getHostnameVerification().ifPresent(v -> n.put("hostname-verification", v));
return n;
}
- /**
- * Whether the engine factory in force validates host names, or {@link Optional#empty()} when it
- * is not one this class recognizes.
- *
- * Read by naming the driver's own factories rather than through an accessor on {@link
- * SslEngineFactory}, deliberately: the interface obliges nobody to answer, so a default answer
- * there would have described a security control on behalf of every implementation that never
- * considered the question — including the ones that misdescribe it. Unknown is instead said by
- * omission, which is what the schema's optional field is for.
- *
- *
{@code instanceof}, not the exact-class checks the policy branches use, and for the same
- * reason as {@link #nodeLocation}: those decide which built-in is in force and must not
- * be fooled by a subclass, whereas this one reads a value the factory already holds, and a
- * subclass inherits it along with the {@code newSslEngine} that acts on it. A subclass that
- * overrides {@code newSslEngine} to configure the engine differently — the only way to break that
- * — reports its parent's answer; extending one of these factories is documented as a way to reuse
- * it, not to invert it.
- */
- private static Optional hostnameValidation(@Nullable SslEngineFactory engineFactory) {
- if (engineFactory instanceof DefaultSslEngineFactory) {
- return Optional.of(((DefaultSslEngineFactory) engineFactory).isHostnameValidationRequired());
- } else if (engineFactory instanceof ProgrammaticSslEngineFactory) {
- return Optional.of(
- ((ProgrammaticSslEngineFactory) engineFactory).isHostnameValidationRequired());
- } else if (engineFactory instanceof SniSslEngineFactory) {
- // No accessor to read: SniSslEngineFactory sets the "HTTPS" endpoint identification algorithm
- // on every engine it builds, unconditionally.
- return Optional.of(true);
- }
- return Optional.empty();
- }
-
/**
* A duration in milliseconds, floored at 1 for any strictly positive duration, and 0 for a zero
* or negative one.
diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java
index bbabe2c8b3f..c4783e7c916 100644
--- a/core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java
+++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java
@@ -17,22 +17,63 @@
*/
package com.datastax.oss.driver.internal.core.context;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.Map;
+import java.util.Optional;
/**
* Adds the {@code DRIVER_CONFIG} entry to the control connection's CQL {@code STARTUP} options, so
* ScyllaDB can store it in {@code system.clients.client_options} and operators can inspect the
* driver's effective settings while investigating incidents.
*
- * The blob describes the whole session, so only the control connection carries it — pooled
- * connections are correlated back to it through the {@link StartupOptionsBuilder#SESSION_ID_KEY
- * SESSION_ID} startup option, which the driver sends on every connection unconditionally and
- * independently of this reporter.
+ *
Most of the blob describes the whole session; its TLS group describes the control connection
+ * carrying it. Pooled connections are correlated back to that control connection through the {@link
+ * StartupOptionsBuilder#SESSION_ID_KEY SESSION_ID} startup option, which the driver sends on every
+ * connection unconditionally and independently of this reporter.
*
*
Governed by {@code advanced.driver-config-reporting.enabled} (enabled by default).
*/
public interface DriverConfigReporter {
+ /** Immutable snapshot of the effective TLS state of the reporting control connection. */
+ final class TlsInfo {
+ private static final TlsInfo DISABLED = new TlsInfo(false, Optional.empty());
+ private static final TlsInfo ENABLED_UNKNOWN = new TlsInfo(true, Optional.empty());
+
+ private final boolean enabled;
+ private final Optional hostnameVerification;
+
+ private TlsInfo(boolean enabled, Optional hostnameVerification) {
+ this.enabled = enabled;
+ this.hostnameVerification = hostnameVerification;
+ }
+
+ /** Returns a snapshot for a connection without a Netty {@code SslHandler}. */
+ public static TlsInfo disabled() {
+ return DISABLED;
+ }
+
+ /** Returns a snapshot for a TLS connection whose hostname-verification state is known. */
+ public static TlsInfo enabled(boolean hostnameVerification) {
+ return new TlsInfo(true, Optional.of(hostnameVerification));
+ }
+
+ /** Returns a snapshot for a TLS connection whose hostname-verification state is unknown. */
+ public static TlsInfo enabledWithUnknownHostnameVerification() {
+ return ENABLED_UNKNOWN;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /** Empty when TLS is disabled or its active engine could not expose this state. */
+ @NonNull
+ public Optional getHostnameVerification() {
+ return hostnameVerification;
+ }
+ }
+
/**
* Adds the {@code DRIVER_CONFIG} blob to the given startup options, unless configuration
* reporting is disabled.
@@ -43,8 +84,13 @@ public interface DriverConfigReporter {
* failure to build the report must be swallowed (and logged) rather than propagated, otherwise it
* would prevent the session from establishing or reconnecting.
*
- * The report describes the driver's own configuration only, so nothing here depends on which
- * backend answered: it can be built before the connection learns anything about its peer.
+ *
The report describes the driver's own configuration and the supplied effective TLS state of
+ * the control connection. The caller must capture that state after channel customization and
+ * immediately before this method is invoked.
+ *
+ * @param startupOptions startup options to add the report to
+ * @param tlsInfo immutable snapshot of the control connection's effective TLS state
*/
- void populateControlConnectionOptions(Map startupOptions);
+ void populateControlConnectionOptions(
+ @NonNull Map startupOptions, @NonNull TlsInfo tlsInfo);
}
diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/NoopDriverConfigReporter.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/NoopDriverConfigReporter.java
index 213c3657585..eed8b28b00a 100644
--- a/core/src/main/java/com/datastax/oss/driver/internal/core/context/NoopDriverConfigReporter.java
+++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/NoopDriverConfigReporter.java
@@ -17,6 +17,7 @@
*/
package com.datastax.oss.driver.internal.core.context;
+import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.Map;
import net.jcip.annotations.ThreadSafe;
@@ -41,7 +42,8 @@
public class NoopDriverConfigReporter implements DriverConfigReporter {
@Override
- public void populateControlConnectionOptions(Map startupOptions) {
+ public void populateControlConnectionOptions(
+ @NonNull Map startupOptions, @NonNull TlsInfo tlsInfo) {
// nothing to do
}
}
diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryPipelineTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryPipelineTest.java
new file mode 100644
index 00000000000..ba2ef2bb52b
--- /dev/null
+++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryPipelineTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.datastax.oss.driver.internal.core.channel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.datastax.oss.driver.api.core.DefaultProtocolVersion;
+import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
+import com.datastax.oss.driver.api.core.config.DriverConfig;
+import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
+import com.datastax.oss.driver.internal.core.context.InternalDriverContext;
+import com.datastax.oss.driver.internal.core.context.NettyOptions;
+import com.datastax.oss.driver.internal.core.metrics.MetricsFactory;
+import com.datastax.oss.driver.internal.core.metrics.NoopNodeMetricUpdater;
+import com.datastax.oss.driver.internal.core.metrics.NoopSessionMetricUpdater;
+import com.datastax.oss.driver.internal.core.ssl.SslHandlerFactory;
+import com.datastax.oss.protocol.internal.FrameCodec;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.ssl.SslHandler;
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.Test;
+
+public class ChannelFactoryPipelineTest {
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void should_install_ssl_before_protocol_init_and_channel_customization() {
+ InternalDriverContext context = mock(InternalDriverContext.class);
+ DriverConfig config = mock(DriverConfig.class);
+ DriverExecutionProfile profile = mock(DriverExecutionProfile.class);
+ NettyOptions nettyOptions = mock(NettyOptions.class);
+ MetricsFactory metricsFactory = mock(MetricsFactory.class);
+ SslHandlerFactory sslHandlerFactory = mock(SslHandlerFactory.class);
+ SslHandler sslHandler = mock(SslHandler.class);
+ FrameCodec frameCodec = mock(FrameCodec.class);
+
+ when(context.getSessionName()).thenReturn("test");
+ when(context.getConfig()).thenReturn(config);
+ when(config.getDefaultProfile()).thenReturn(profile);
+ when(profile.isDefined(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn(false);
+ when(profile.getDuration(DefaultDriverOption.CONNECTION_SET_KEYSPACE_TIMEOUT))
+ .thenReturn(Duration.ofSeconds(1));
+ when(profile.getDuration(DefaultDriverOption.CONNECTION_INIT_QUERY_TIMEOUT))
+ .thenReturn(Duration.ofSeconds(1));
+ when(profile.getDuration(DefaultDriverOption.HEARTBEAT_INTERVAL))
+ .thenReturn(Duration.ofSeconds(30));
+ when(profile.getBytes(DefaultDriverOption.PROTOCOL_MAX_FRAME_LENGTH)).thenReturn(1024L);
+ when(profile.getInt(DefaultDriverOption.CONNECTION_MAX_REQUESTS)).thenReturn(128);
+ when(profile.getInt(DefaultDriverOption.CONNECTION_MAX_ORPHAN_REQUESTS)).thenReturn(32);
+ when(context.getSslHandlerFactory()).thenReturn(Optional.of(sslHandlerFactory));
+ when(sslHandlerFactory.newSslHandler(any(), eq(ChannelFactoryTestBase.SERVER_ADDRESS)))
+ .thenReturn(sslHandler);
+ when(context.getMetricsFactory()).thenReturn(metricsFactory);
+ when(metricsFactory.getSessionUpdater()).thenReturn(NoopSessionMetricUpdater.INSTANCE);
+ when(context.getFrameCodec()).thenReturn(frameCodec);
+ when(context.getNettyOptions()).thenReturn(nettyOptions);
+
+ AtomicBoolean observedCompletePipeline = new AtomicBoolean();
+ doAnswer(
+ invocation -> {
+ Channel channel = invocation.getArgument(0);
+ List names = channel.pipeline().names();
+ assertThat(channel.pipeline().get(ChannelFactory.SSL_HANDLER_NAME))
+ .isSameAs(sslHandler);
+ assertThat(names.indexOf(ChannelFactory.SSL_HANDLER_NAME))
+ .isLessThan(names.indexOf(ChannelFactory.INIT_HANDLER_NAME));
+ observedCompletePipeline.set(true);
+ return null;
+ })
+ .when(nettyOptions)
+ .afterChannelInitialized(any());
+
+ ChannelFactory factory = new ChannelFactory(context);
+ CompletableFuture resultFuture = new CompletableFuture<>();
+ ChannelInitializer initializer =
+ factory.initializer(
+ ChannelFactoryTestBase.SERVER_ADDRESS,
+ DefaultProtocolVersion.V4,
+ DriverChannelOptions.DEFAULT,
+ NoopNodeMetricUpdater.INSTANCE,
+ resultFuture);
+
+ EmbeddedChannel channel = new EmbeddedChannel(initializer);
+ try {
+ assertThat(observedCompletePipeline).isTrue();
+ assertThat(resultFuture).isNotCompletedExceptionally();
+ verify(nettyOptions).afterChannelInitialized(channel);
+ } finally {
+ channel.finishAndReleaseAll();
+ }
+ }
+}
diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java
index ed6668a6c83..0bf4b762aa9 100644
--- a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java
+++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java
@@ -142,7 +142,7 @@ public void setup() throws InterruptedException {
when(context.getWriteCoalescer()).thenReturn(new PassThroughWriteCoalescer(null));
when(context.getCompressor()).thenReturn(compressor);
// The init handler consults the config reporter for the control connection; default to a no-op.
- when(context.getDriverConfigReporter()).thenReturn(startupOptions -> {});
+ when(context.getDriverConfigReporter()).thenReturn((startupOptions, tlsInfo) -> {});
// Start local server
ServerBootstrap serverBootstrap =
diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandlerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandlerTest.java
index 682caac198d..30ac4d2cbc5 100644
--- a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandlerTest.java
+++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandlerTest.java
@@ -19,6 +19,7 @@
import static com.datastax.oss.driver.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -43,6 +44,7 @@
import com.datastax.oss.driver.internal.core.TestResponses;
import com.datastax.oss.driver.internal.core.context.DefaultDriverConfigReporter;
import com.datastax.oss.driver.internal.core.context.DriverConfigReporter;
+import com.datastax.oss.driver.internal.core.context.DriverConfigReporter.TlsInfo;
import com.datastax.oss.driver.internal.core.context.InternalDriverContext;
import com.datastax.oss.driver.internal.core.context.StartupOptionsBuilder;
import com.datastax.oss.driver.internal.core.metadata.TestNodeFactory;
@@ -64,6 +66,8 @@
import com.datastax.oss.protocol.internal.response.result.SetKeyspace;
import com.datastax.oss.protocol.internal.util.Bytes;
import io.netty.channel.ChannelFuture;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.ssl.SslHandler;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.time.Duration;
@@ -72,8 +76,12 @@
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.slf4j.LoggerFactory;
@@ -107,7 +115,8 @@ public void setup() {
.thenReturn(Duration.ofSeconds(30));
when(internalDriverContext.getProtocolVersionRegistry()).thenReturn(protocolVersionRegistry);
// The init handler consults the config reporter for the control connection; default to a no-op.
- when(internalDriverContext.getDriverConfigReporter()).thenReturn(startupOptions -> {});
+ when(internalDriverContext.getDriverConfigReporter())
+ .thenReturn((startupOptions, tlsInfo) -> {});
channel
.pipeline()
@@ -160,17 +169,24 @@ public void should_initialize() {
}
// Mirrors the real reporter, which only ever sees the control connection.
- private void stubConfigReporter() {
- when(internalDriverContext.getDriverConfigReporter())
- .thenReturn(
- startupOptions ->
- startupOptions.put(
- DefaultDriverConfigReporter.DRIVER_CONFIG_KEY, "{\"version\":1}"));
+ private DriverConfigReporter stubConfigReporter() {
+ DriverConfigReporter reporter = mock(DriverConfigReporter.class);
+ doAnswer(
+ invocation -> {
+ invocation
+ .