This is an automated email from the ASF dual-hosted git repository.
anmolnar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zookeeper.git
The following commit(s) were added to refs/heads/master by this push:
new 9102d0be1 ZOOKEEPER-4835: Make use of Netty optional when no SSL is
used
9102d0be1 is described below
commit 9102d0be12e66a5c592d8cbf0c6865f68ffa9252
Author: David Smiley <[email protected]>
AuthorDate: Fri Sep 11 15:05:04 2026 -0400
ZOOKEEPER-4835: Make use of Netty optional when no SSL is used
Reviewers: PDavid, PDavid, anmolnar
Author: dsmiley
Closes #2374 from dsmiley/master
---
pom.xml | 7 +
zookeeper-assembly/pom.xml | 15 ++
zookeeper-server/pom.xml | 8 +
.../apache/zookeeper/ClientCnxnSocketNetty.java | 4 +-
.../main/java/org/apache/zookeeper/ZooKeeper.java | 24 ++-
.../zookeeper/cli/HexDumpOutputFormatter.java | 40 +++-
...lientX509Util.java => ClientNettyX509Util.java} | 43 ++---
.../apache/zookeeper/common/ClientX509Util.java | 205 +--------------------
.../java/org/apache/zookeeper/common/X509Util.java | 20 +-
.../zookeeper/server/NettyServerCnxnFactory.java | 9 +-
.../apache/zookeeper/server/ServerCnxnFactory.java | 45 +++++
.../apache/zookeeper/server/ZooKeeperServer.java | 4 +-
.../zookeeper/server/ZooKeeperServerMain.java | 2 +-
.../apache/zookeeper/server/quorum/QuorumPeer.java | 2 +-
.../zookeeper/server/quorum/QuorumPeerMain.java | 2 +-
.../apache/zookeeper/NettyOptionalArchTest.java | 66 +++++++
.../zookeeper/cli/HexDumpOutputFormatterTest.java | 43 +++++
.../org/apache/zookeeper/common/X509UtilTest.java | 14 +-
18 files changed, 282 insertions(+), 271 deletions(-)
diff --git a/pom.xml b/pom.xml
index 60b9903d5..c4343539a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -563,6 +563,7 @@
<commons-io.version>2.17.0</commons-io.version>
<burningwave.mockdns.version>0.27.2</burningwave.mockdns.version>
<dnsjava.version>3.5.1</dnsjava.version>
+ <archunit.version>1.4.2</archunit.version>
<clover-maven-plugin.version>4.4.1</clover-maven-plugin.version>
<sonar-maven-plugin.version>3.7.0.1746</sonar-maven-plugin.version>
<maven.min.version>3.5.0</maven.min.version>
@@ -695,6 +696,12 @@
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
</dependency>
+ <dependency>
+ <groupId>com.tngtech.archunit</groupId>
+ <artifactId>archunit-junit5</artifactId>
+ <version>${archunit.version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-bom</artifactId>
diff --git a/zookeeper-assembly/pom.xml b/zookeeper-assembly/pom.xml
index 1779c3c47..c92c522c7 100644
--- a/zookeeper-assembly/pom.xml
+++ b/zookeeper-assembly/pom.xml
@@ -119,6 +119,21 @@
<groupId>org.xerial.snappy</groupId>
<artifactId>snappy-java</artifactId>
</dependency>
+ <!-- Netty is an optional dependency of zookeeper-server but should be
+ included in the server distribution for SSL/TLS support. -->
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-handler</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-transport-native-epoll</artifactId>
+ <classifier>linux-x86_64</classifier>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-tcnative-boringssl-static</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/zookeeper-server/pom.xml b/zookeeper-server/pom.xml
index d06138838..71c3e3b57 100644
--- a/zookeeper-server/pom.xml
+++ b/zookeeper-server/pom.xml
@@ -65,15 +65,18 @@
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
+ <optional>true</optional>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<classifier>linux-x86_64</classifier>
+ <optional>true</optional>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
+ <optional>true</optional>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
@@ -180,6 +183,11 @@
<artifactId>tools</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.tngtech.archunit</groupId>
+ <artifactId>archunit-junit5</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.xerial.snappy</groupId>
<artifactId>snappy-java</artifactId>
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxnSocketNetty.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxnSocketNetty.java
index cc8f5a908..075ea37a3 100644
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxnSocketNetty.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxnSocketNetty.java
@@ -51,7 +51,7 @@
import org.apache.zookeeper.ClientCnxn.EndOfStreamException;
import org.apache.zookeeper.ClientCnxn.Packet;
import org.apache.zookeeper.client.ZKClientConfig;
-import org.apache.zookeeper.common.ClientX509Util;
+import org.apache.zookeeper.common.ClientNettyX509Util;
import org.apache.zookeeper.common.NettyUtils;
import org.apache.zookeeper.common.X509Exception;
import org.slf4j.Logger;
@@ -446,7 +446,7 @@ private synchronized void initSSL(ChannelPipeline pipeline)
throws X509Exception.SSLContextException,
X509Exception.KeyManagerException,
X509Exception.TrustManagerException, SSLException {
if (sslContext == null) {
- try (ClientX509Util x509Util = new ClientX509Util()) {
+ try (ClientNettyX509Util x509Util = new ClientNettyX509Util())
{
sslContext =
x509Util.createNettySslContextForClient(clientConfig);
}
}
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java
index 239f97b0f..5bc7a7b98 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java
@@ -3141,11 +3141,20 @@ protected SocketAddress testableLocalSocketAddress() {
}
private ClientCnxnSocket getClientCnxnSocket() throws IOException {
+ // Derived from a same-package class rather than a string literal so
the shade plugin relocates it.
+ final String nettyClientCnxnSocketName =
ClientCnxnSocketNIO.class.getPackageName() + ".ClientCnxnSocketNetty";
String clientCnxnSocketName =
getClientConfig().getProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET);
- if (clientCnxnSocketName == null ||
clientCnxnSocketName.equals(ClientCnxnSocketNIO.class.getSimpleName())) {
+ if (clientCnxnSocketName == null) {
+ boolean secureClient =
getClientConfig().getBoolean(ZKClientConfig.SECURE_CLIENT);
+ if (secureClient) {
+ clientCnxnSocketName = nettyClientCnxnSocketName;
+ } else {
+ clientCnxnSocketName = ClientCnxnSocketNIO.class.getName();
+ }
+ } else if
(clientCnxnSocketName.equals(ClientCnxnSocketNIO.class.getSimpleName())) {
clientCnxnSocketName = ClientCnxnSocketNIO.class.getName();
- } else if
(clientCnxnSocketName.equals(ClientCnxnSocketNetty.class.getSimpleName())) {
- clientCnxnSocketName = ClientCnxnSocketNetty.class.getName();
+ } else if (clientCnxnSocketName.equals("ClientCnxnSocketNetty")) {
+ clientCnxnSocketName = nettyClientCnxnSocketName;
}
try {
@@ -3153,8 +3162,13 @@ private ClientCnxnSocket getClientCnxnSocket() throws
IOException {
.getDeclaredConstructor(ZKClientConfig.class);
ClientCnxnSocket clientCxnSocket = (ClientCnxnSocket)
clientCxnConstructor.newInstance(getClientConfig());
return clientCxnSocket;
- } catch (Exception e) {
- throw new IOException("Couldn't instantiate " +
clientCnxnSocketName, e);
+ } catch (Exception | NoClassDefFoundError e) {
+ String msg = "Couldn't instantiate " + clientCnxnSocketName;
+ if (getClientConfig().getBoolean(ZKClientConfig.SECURE_CLIENT)) {
+ msg += ". SSL/TLS support requires Netty; please add
netty-handler"
+ + " (and optionally netty-tcnative-boringssl-static) to
your project's dependencies.";
+ }
+ throw new IOException(msg, e);
}
}
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/cli/HexDumpOutputFormatter.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/cli/HexDumpOutputFormatter.java
index d545a422e..a446b54cf 100644
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/cli/HexDumpOutputFormatter.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/cli/HexDumpOutputFormatter.java
@@ -18,17 +18,45 @@
package org.apache.zookeeper.cli;
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.ByteBufUtil;
-import io.netty.buffer.Unpooled;
-
public class HexDumpOutputFormatter implements OutputFormatter {
public static final HexDumpOutputFormatter INSTANCE = new
HexDumpOutputFormatter();
+ private static final int BYTES_PER_ROW = 16;
+ private static final int ASCII_PRINTABLE_MIN = 0x20; // space
+ private static final int ASCII_PRINTABLE_MAX = 0x7f; // DEL (exclusive)
+ private static final String LINE_SEPARATOR = System.lineSeparator();
+ private static final String HEADER_LINE =
+ " +-------------------------------------------------+" +
LINE_SEPARATOR
+ + " | 0 1 2 3 4 5 6 7 8 9 a b c d e f |" +
LINE_SEPARATOR
+ +
"+--------+-------------------------------------------------+----------------+";
+ private static final String FOOTER_LINE =
+
"+--------+-------------------------------------------------+----------------+";
+
@Override
public String format(byte[] data) {
- ByteBuf buf = Unpooled.wrappedBuffer(data);
- return ByteBufUtil.prettyHexDump(buf);
+ if (data == null || data.length == 0) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ sb.append(HEADER_LINE).append(LINE_SEPARATOR);
+ for (int offset = 0; offset < data.length; offset += BYTES_PER_ROW) {
+ sb.append(String.format("|%08x|", offset));
+ StringBuilder charPart = new StringBuilder();
+ for (int i = 0; i < BYTES_PER_ROW; i++) {
+ if (offset + i < data.length) {
+ int b = data[offset + i] & 0xFF;
+ sb.append(String.format(" %02x", b));
+ char c = (char) b;
+ charPart.append(c >= ASCII_PRINTABLE_MIN && c <
ASCII_PRINTABLE_MAX ? c : '.');
+ } else {
+ sb.append(" ");
+ charPart.append(' ');
+ }
+ }
+ sb.append("
|").append(charPart).append("|").append(LINE_SEPARATOR);
+ }
+ sb.append(FOOTER_LINE);
+ return sb.toString();
}
}
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientX509Util.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientNettyX509Util.java
similarity index 90%
copy from
zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientX509Util.java
copy to
zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientNettyX509Util.java
index 1c1d6ca88..227fdde8c 100644
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientX509Util.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientNettyX509Util.java
@@ -37,32 +37,14 @@
import org.slf4j.LoggerFactory;
/**
- * X509 utilities specific for client-server communication framework.
+ * Extends {@link ClientX509Util} with Netty-specific SSL context creation
+ * methods. This class is only loaded when Netty is present on the classpath.
+ * Code that only needs SSL property names should use {@link ClientX509Util}
+ * directly so that Netty remains an optional dependency.
*/
-public class ClientX509Util extends X509Util {
+public class ClientNettyX509Util extends ClientX509Util {
- private static final Logger LOG =
LoggerFactory.getLogger(ClientX509Util.class);
-
- private final String sslAuthProviderProperty = getConfigPrefix() +
"authProvider";
- private final String sslProviderProperty = getConfigPrefix() +
"sslProvider";
-
- @Override
- protected String getConfigPrefix() {
- return "zookeeper.ssl.";
- }
-
- @Override
- protected boolean shouldVerifyClientHostname() {
- return false;
- }
-
- public String getSslAuthProviderProperty() {
- return sslAuthProviderProperty;
- }
-
- public String getSslProviderProperty() {
- return sslProviderProperty;
- }
+ private static final Logger LOG =
LoggerFactory.getLogger(ClientNettyX509Util.class);
public SslContext createNettySslContextForClient(ZKConfig config)
throws X509Exception.SSLContextException,
X509Exception.KeyManagerException,
@@ -131,7 +113,7 @@ public SslContext createNettySslContextForServer(ZKConfig
config, KeyManager key
if (enabledProtocols != null) {
sslContextBuilder.protocols(enabledProtocols);
}
-
sslContextBuilder.clientAuth(getClientAuth(config).toNettyClientAuth());
+ sslContextBuilder.clientAuth(toNettyClientAuth(getClientAuth(config)));
Iterable<String> enabledCiphers = getCipherSuites(config);
if (enabledCiphers != null) {
sslContextBuilder.ciphers(enabledCiphers);
@@ -182,7 +164,7 @@ private SslContext createNettyJdkSslContext(ZKConfig
config, SSLContext sslConte
getCipherSuites(config),
IdentityCipherSuiteFilter.INSTANCE,
null,
- isClient ? X509Util.ClientAuth.NONE.toNettyClientAuth() :
getClientAuth(config).toNettyClientAuth(),
+ isClient ? toNettyClientAuth(X509Util.ClientAuth.NONE) :
toNettyClientAuth(getClientAuth(config)),
getEnabledProtocols(config),
false);
@@ -232,6 +214,15 @@ private X509Util.ClientAuth getClientAuth(final ZKConfig
config) {
return
X509Util.ClientAuth.fromPropertyValue(config.getProperty(getSslClientAuthProperty()));
}
+ private static io.netty.handler.ssl.ClientAuth
toNettyClientAuth(X509Util.ClientAuth clientAuth) {
+ switch (clientAuth) {
+ case NONE: return io.netty.handler.ssl.ClientAuth.NONE;
+ case WANT: return io.netty.handler.ssl.ClientAuth.OPTIONAL;
+ case NEED: return io.netty.handler.ssl.ClientAuth.REQUIRE;
+ default: throw new IllegalArgumentException("Unknown ClientAuth: "
+ clientAuth);
+ }
+ }
+
private Iterable<String> getCipherSuites(final ZKConfig config) {
String cipherSuitesInput =
config.getProperty(getSslCipherSuitesProperty());
if (cipherSuitesInput == null) {
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientX509Util.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientX509Util.java
index 1c1d6ca88..473aab880 100644
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientX509Util.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientX509Util.java
@@ -18,31 +18,15 @@
package org.apache.zookeeper.common;
-import io.netty.handler.ssl.DelegatingSslContext;
-import io.netty.handler.ssl.IdentityCipherSuiteFilter;
-import io.netty.handler.ssl.JdkSslContext;
-import io.netty.handler.ssl.OpenSsl;
-import io.netty.handler.ssl.SslContext;
-import io.netty.handler.ssl.SslContextBuilder;
-import io.netty.handler.ssl.SslProvider;
-import java.security.Security;
-import java.util.Arrays;
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLParameters;
-import javax.net.ssl.TrustManager;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
/**
* X509 utilities specific for client-server communication framework.
+ *
+ * <p>This class is intentionally free of Netty dependencies so it can be
loaded
+ * without Netty on the classpath. For Netty SSL context creation use
+ * {@link ClientNettyX509Util}.
*/
public class ClientX509Util extends X509Util {
- private static final Logger LOG =
LoggerFactory.getLogger(ClientX509Util.class);
-
private final String sslAuthProviderProperty = getConfigPrefix() +
"authProvider";
private final String sslProviderProperty = getConfigPrefix() +
"sslProvider";
@@ -63,185 +47,4 @@ public String getSslAuthProviderProperty() {
public String getSslProviderProperty() {
return sslProviderProperty;
}
-
- public SslContext createNettySslContextForClient(ZKConfig config)
- throws X509Exception.SSLContextException,
X509Exception.KeyManagerException,
- X509Exception.TrustManagerException, SSLException {
- SSLContext suppliedSSLContext = loadSuppliedSSLContext(config);
- if (suppliedSSLContext != null) {
- return createNettyJdkSslContext(config, suppliedSSLContext, true);
- }
-
- SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
-
- KeyManager km = buildClientKeyManager(config);
- if (km != null) {
- sslContextBuilder.keyManager(km);
- }
-
- TrustManager tm = buildTrustManager(config);
- if (tm != null) {
- sslContextBuilder.trustManager(tm);
- }
-
- handleTcnativeOcspStapling(sslContextBuilder, config);
- String[] enabledProtocols = getEnabledProtocols(config);
- if (enabledProtocols != null) {
- sslContextBuilder.protocols(enabledProtocols);
- }
- Iterable<String> enabledCiphers = getCipherSuites(config);
- if (enabledCiphers != null) {
- sslContextBuilder.ciphers(enabledCiphers);
- }
- sslContextBuilder.sslProvider(getSslProvider(config));
-
- SslContext sslContext1 = sslContextBuilder.build();
-
- if ((getFipsMode(config) || tm == null) &&
isServerHostnameVerificationEnabled(config)) {
- return addHostnameVerification(sslContext1, "Server");
- } else {
- return sslContext1;
- }
- }
-
- public SslContext createNettySslContextForServer(ZKConfig config)
- throws X509Exception.SSLContextException,
X509Exception.KeyManagerException, X509Exception.TrustManagerException,
SSLException {
- SSLContext suppliedSSLContext = loadSuppliedSSLContext(config);
- if (suppliedSSLContext != null) {
- return createNettyJdkSslContext(config, suppliedSSLContext, false);
- }
-
- KeyManager km = buildKeyManager(config);
- if (km == null) {
- throw new X509Exception.SSLContextException(
- "Keystore is required for SSL server: " +
getSslKeystoreLocationProperty());
- }
- return createNettySslContextForServer(config, km,
buildServerTrustManager(config));
- }
-
- public SslContext createNettySslContextForServer(ZKConfig config,
KeyManager keyManager, TrustManager trustManager) throws SSLException {
- SslContextBuilder sslContextBuilder =
SslContextBuilder.forServer(keyManager);
-
- if (trustManager != null) {
- sslContextBuilder.trustManager(trustManager);
- }
-
- handleTcnativeOcspStapling(sslContextBuilder, config);
- String[] enabledProtocols = getEnabledProtocols(config);
- if (enabledProtocols != null) {
- sslContextBuilder.protocols(enabledProtocols);
- }
-
sslContextBuilder.clientAuth(getClientAuth(config).toNettyClientAuth());
- Iterable<String> enabledCiphers = getCipherSuites(config);
- if (enabledCiphers != null) {
- sslContextBuilder.ciphers(enabledCiphers);
- }
- sslContextBuilder.sslProvider(getSslProvider(config));
-
- SslContext sslContext1 = sslContextBuilder.build();
-
- if ((getFipsMode(config) || trustManager == null) &&
isClientHostnameVerificationEnabled(config)) {
- return addHostnameVerification(sslContext1, "Client");
- } else {
- return sslContext1;
- }
- }
-
- /**
- * Wraps a user supplied {@link SSLContext} in a Netty {@link SslContext},
applying the configured
- * protocols, cipher suites, client auth mode and hostname verification on
top of it.
- *
- * <p>A supplied SSLContext carries its own key and trust managers, so it
can only be used with the
- * JDK SSL provider: the OpenSSL providers build their own native context
and cannot delegate to it.
- *
- * <p>Unlike the file based path, hostname verification is applied
whenever it is enabled. The file
- * based path relies on {@link ZKTrustManager} to verify hostnames and
only falls back to endpoint
- * identification when no trust manager is available, which is never the
case for a supplied context.
- *
- * @param config the configuration to read the SSL options from.
- * @param sslContext the user supplied SSLContext.
- * @param isClient {@code true} to create a client side context, {@code
false} for server side.
- * @return the Netty SslContext.
- * @throws X509Exception.SSLContextException if a non JDK SSL provider is
configured.
- */
- private SslContext createNettyJdkSslContext(ZKConfig config, SSLContext
sslContext, boolean isClient)
- throws X509Exception.SSLContextException {
- SslProvider sslProvider = getSslProvider(config);
- if (sslProvider != SslProvider.JDK) {
- throw new X509Exception.SSLContextException("An SSLContext
supplied through "
- +
getSslContextSupplierClassProperty()
- + " can only be used
with the JDK SSL provider, but "
- +
getSslProviderProperty()
- + " is set to "
- + sslProvider);
- }
-
- SslContext nettySslContext = new JdkSslContext(
- sslContext,
- isClient,
- getCipherSuites(config),
- IdentityCipherSuiteFilter.INSTANCE,
- null,
- isClient ? X509Util.ClientAuth.NONE.toNettyClientAuth() :
getClientAuth(config).toNettyClientAuth(),
- getEnabledProtocols(config),
- false);
-
- boolean hostnameVerificationEnabled = isClient
- ? isServerHostnameVerificationEnabled(config)
- : isClientHostnameVerificationEnabled(config);
- if (hostnameVerificationEnabled) {
- return addHostnameVerification(nettySslContext, isClient ?
"Server" : "Client");
- }
- return nettySslContext;
- }
-
- private SslContextBuilder handleTcnativeOcspStapling(SslContextBuilder
builder, ZKConfig config) {
- SslProvider sslProvider = getSslProvider(config);
- boolean tcnative = sslProvider == SslProvider.OPENSSL || sslProvider
== SslProvider.OPENSSL_REFCNT;
- boolean ocspEnabled = config.getBoolean(getSslOcspEnabledProperty(),
Boolean.parseBoolean(Security.getProperty("ocsp.enable")));
-
- if (tcnative && ocspEnabled && OpenSsl.isOcspSupported()) {
- builder.enableOcsp(ocspEnabled);
- }
- return builder;
- }
-
- private SslContext addHostnameVerification(SslContext sslContext, String
clientOrServer) {
- return new DelegatingSslContext(sslContext) {
- @Override
- protected void initEngine(SSLEngine sslEngine) {
- SSLParameters sslParameters = sslEngine.getSSLParameters();
- sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
- sslEngine.setSSLParameters(sslParameters);
- if (LOG.isDebugEnabled()) {
- LOG.debug("{} hostname verification: enabled HTTPS style
endpoint identification algorithm", clientOrServer);
- }
- }
- };
- }
-
- private String[] getEnabledProtocols(final ZKConfig config) {
- String enabledProtocolsInput =
config.getProperty(getSslEnabledProtocolsProperty());
- if (enabledProtocolsInput == null) {
- return null;
- }
- return enabledProtocolsInput.split(",");
- }
-
- private X509Util.ClientAuth getClientAuth(final ZKConfig config) {
- return
X509Util.ClientAuth.fromPropertyValue(config.getProperty(getSslClientAuthProperty()));
- }
-
- private Iterable<String> getCipherSuites(final ZKConfig config) {
- String cipherSuitesInput =
config.getProperty(getSslCipherSuitesProperty());
- if (cipherSuitesInput == null) {
- return null;
- } else {
- return Arrays.asList(cipherSuitesInput.split(","));
- }
- }
-
- public SslProvider getSslProvider(ZKConfig config) {
- return
SslProvider.valueOf(config.getProperty(getSslProviderProperty(), "JDK"));
- }
}
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
index 993404f78..e6cd00136 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
@@ -55,7 +55,6 @@
import org.apache.zookeeper.common.X509Exception.KeyManagerException;
import org.apache.zookeeper.common.X509Exception.SSLContextException;
import org.apache.zookeeper.common.X509Exception.TrustManagerException;
-import org.apache.zookeeper.server.NettyServerCnxnFactory;
import org.apache.zookeeper.server.auth.ProviderRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -69,6 +68,7 @@ public abstract class X509Util implements Closeable,
AutoCloseable {
private static final String REJECT_CLIENT_RENEGOTIATION_PROPERTY =
"jdk.tls.rejectClientInitiatedRenegotiation";
public static final String FIPS_MODE_PROPERTY = "zookeeper.fips-mode";
+ public static final String CLIENT_CERT_RELOAD_KEY =
"zookeeper.client.certReload";
private static final boolean FIPS_MODE_DEFAULT = true;
public static final String TLS_1_1 = "TLSv1.1";
public static final String TLS_1_2 = "TLSv1.2";
@@ -133,15 +133,9 @@ public static String defaultTlsProtocol(ZKConfig config) {
* If the config property is not set, the default value is NEED.
*/
public enum ClientAuth {
- NONE(io.netty.handler.ssl.ClientAuth.NONE),
- WANT(io.netty.handler.ssl.ClientAuth.OPTIONAL),
- NEED(io.netty.handler.ssl.ClientAuth.REQUIRE);
-
- private final io.netty.handler.ssl.ClientAuth nettyAuth;
-
- ClientAuth(io.netty.handler.ssl.ClientAuth nettyAuth) {
- this.nettyAuth = nettyAuth;
- }
+ NONE,
+ WANT,
+ NEED;
/**
* Converts a property value to a ClientAuth enum. If the input string
is empty or null, returns
@@ -156,10 +150,6 @@ public static ClientAuth fromPropertyValue(String prop) {
}
return ClientAuth.valueOf(prop.toUpperCase());
}
-
- public io.netty.handler.ssl.ClientAuth toNettyClientAuth() {
- return nettyAuth;
- }
}
private final String sslProtocolProperty = getConfigPrefix() + "protocol";
@@ -371,7 +361,7 @@ private void resetDefaultSSLContextAndOptions() throws
X509Exception.SSLContextE
SSLContextAndOptions newContext = createSSLContextAndOptions();
defaultSSLContextAndOptions.set(newContext);
- if (Boolean.getBoolean(NettyServerCnxnFactory.CLIENT_CERT_RELOAD_KEY))
{
+ if (Boolean.getBoolean(CLIENT_CERT_RELOAD_KEY)) {
ProviderRegistry.addOrUpdateProvider(ProviderRegistry.AUTHPROVIDER_PROPERTY_PREFIX
+ "x509");
}
}
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/NettyServerCnxnFactory.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/NettyServerCnxnFactory.java
index 5198e91c1..2b412cda9 100644
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/NettyServerCnxnFactory.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/NettyServerCnxnFactory.java
@@ -61,11 +61,12 @@
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import org.apache.zookeeper.KeeperException;
-import org.apache.zookeeper.common.ClientX509Util;
+import org.apache.zookeeper.common.ClientNettyX509Util;
import org.apache.zookeeper.common.ConfigException;
import org.apache.zookeeper.common.NettyUtils;
import org.apache.zookeeper.common.X509Exception;
import org.apache.zookeeper.common.X509Exception.SSLContextException;
+import org.apache.zookeeper.common.X509Util;
import org.apache.zookeeper.common.ZKConfig;
import org.apache.zookeeper.server.NettyServerCnxn.HandshakeState;
import org.apache.zookeeper.server.auth.ProviderRegistry;
@@ -115,7 +116,7 @@ public void setOutstandingHandshakeLimit(int limit) {
private InetSocketAddress localAddress;
private int maxClientCnxns = 60;
int listenBacklog = -1;
- private final ClientX509Util x509Util;
+ private final ClientNettyX509Util x509Util;
public static final String NETTY_ADVANCED_FLOW_CONTROL =
"zookeeper.netty.advancedFlowControl.enabled";
private boolean advancedFlowControlEnabled = false;
@@ -124,7 +125,7 @@ public void setOutstandingHandshakeLimit(int limit) {
private static final AtomicReference<ByteBufAllocator> TEST_ALLOCATOR =
new AtomicReference<>(null);
- public static final String CLIENT_CERT_RELOAD_KEY =
"zookeeper.client.certReload";
+ public static final String CLIENT_CERT_RELOAD_KEY =
X509Util.CLIENT_CERT_RELOAD_KEY;
/**
* A handler that detects whether the client would like to use
@@ -514,7 +515,7 @@ private ServerBootstrap
configureBootstrapAllocator(ServerBootstrap bootstrap) {
}
NettyServerCnxnFactory() {
- x509Util = new ClientX509Util();
+ x509Util = new ClientNettyX509Util();
boolean useClientReload = Boolean.getBoolean(CLIENT_CERT_RELOAD_KEY);
LOG.info("{}={}", CLIENT_CERT_RELOAD_KEY, useClientReload);
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/ServerCnxnFactory.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/ServerCnxnFactory.java
index f63c1eec4..32f036eb0 100644
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/ServerCnxnFactory.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/ServerCnxnFactory.java
@@ -161,6 +161,14 @@ public final void setZooKeeperServer(ZooKeeperServer zks) {
public abstract void closeAll(ServerCnxn.DisconnectReason reason);
+ /**
+ * Returns the number of connections that are currently performing a TLS
handshake.
+ * Non-Netty implementations return 0.
+ */
+ public int getOutstandingHandshakeNum() {
+ return 0;
+ }
+
/**
* Attempts to shed approximately the specified percentage of connections.
*
@@ -228,6 +236,43 @@ public static ServerCnxnFactory createFactory() throws
IOException {
}
}
+ /**
+ * Creates a ServerCnxnFactory, defaulting to NettyServerCnxnFactory when
+ * {@code secure} is {@code true} and no explicit factory is configured via
+ * the {@value #ZOOKEEPER_SERVER_CNXN_FACTORY} system property. SSL/TLS
+ * requires Netty; if Netty is not present on the classpath a helpful
+ * {@link IOException} is thrown.
+ *
+ * @param secure {@code true} when the factory will be used for secure
(SSL/TLS) connections
+ * @return a new ServerCnxnFactory instance
+ * @throws IOException if the factory cannot be instantiated
+ */
+ public static ServerCnxnFactory createFactory(boolean secure) throws
IOException {
+ String serverCnxnFactoryName =
System.getProperty(ZOOKEEPER_SERVER_CNXN_FACTORY);
+ if (serverCnxnFactoryName == null) {
+ if (secure) {
+ // Derived from a same-package class rather than a string
literal so the shade plugin relocates it.
+ serverCnxnFactoryName =
NIOServerCnxnFactory.class.getPackageName() + ".NettyServerCnxnFactory";
+ } else {
+ serverCnxnFactoryName = NIOServerCnxnFactory.class.getName();
+ }
+ }
+ try {
+ ServerCnxnFactory serverCnxnFactory = (ServerCnxnFactory)
Class.forName(serverCnxnFactoryName)
+
.getDeclaredConstructor()
+
.newInstance();
+ LOG.info("Using {} as server connection factory",
serverCnxnFactoryName);
+ return serverCnxnFactory;
+ } catch (Exception | NoClassDefFoundError e) {
+ String msg = "Couldn't instantiate " + serverCnxnFactoryName;
+ if (secure) {
+ msg += ". SSL/TLS support requires Netty; please add
netty-handler"
+ + " (and optionally netty-tcnative-boringssl-static) to
your project's dependencies.";
+ }
+ throw new IOException(msg, e);
+ }
+ }
+
public static ServerCnxnFactory createFactory(int clientPort, int
maxClientCnxns) throws IOException {
return createFactory(new InetSocketAddress(clientPort),
maxClientCnxns, -1);
}
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServer.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServer.java
index 9545730cb..5c7f5f4ea 100644
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServer.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServer.java
@@ -2389,8 +2389,8 @@ public boolean authWriteRequest(Request request) {
}
public int getOutstandingHandshakeNum() {
- if (serverCnxnFactory instanceof NettyServerCnxnFactory) {
- return ((NettyServerCnxnFactory)
serverCnxnFactory).getOutstandingHandshakeNum();
+ if (serverCnxnFactory != null) {
+ return serverCnxnFactory.getOutstandingHandshakeNum();
} else {
return 0;
}
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServerMain.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServerMain.java
index c721e685b..0545827c8 100644
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServerMain.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServerMain.java
@@ -162,7 +162,7 @@ public void runFromConfig(ServerConfig config) throws
IOException, AdminServerEx
needStartZKServer = false;
}
if (config.getSecureClientPortAddress() != null) {
- secureCnxnFactory = ServerCnxnFactory.createFactory();
+ secureCnxnFactory = ServerCnxnFactory.createFactory(true);
secureCnxnFactory.configure(config.getSecureClientPortAddress(),
config.getMaxClientCnxns(), config.getClientPortListenBacklog(), true);
secureCnxnFactory.startup(zkServer, needStartZKServer);
}
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeer.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeer.java
index 56dcbd224..dd599d9a9 100644
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeer.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeer.java
@@ -2429,7 +2429,7 @@ public boolean processReconfig(QuorumVerifier qv, Long
suggestedLeaderId, Long z
// start secureCnxnFactory first
try {
configureSSLAuth();
- secureCnxnFactory =
ServerCnxnFactory.createFactory();
+ secureCnxnFactory =
ServerCnxnFactory.createFactory(true);
secureCnxnFactory.configure(myNewQS.secureClientAddr, getMaxClientCnxns(),
getClientPortListenBacklog(), true);
secureCnxnFactory.start();
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java
index 86b3b8a7c..3fb3d6bf3 100644
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java
@@ -170,7 +170,7 @@ public void runFromConfig(QuorumPeerConfig config) throws
IOException, AdminServ
}
if (config.getSecureClientPortAddress() != null) {
- secureCnxnFactory = ServerCnxnFactory.createFactory();
+ secureCnxnFactory = ServerCnxnFactory.createFactory(true);
secureCnxnFactory.configure(config.getSecureClientPortAddress(),
config.getMaxClientCnxns(), config.getClientPortListenBacklog(), true);
}
diff --git
a/zookeeper-server/src/test/java/org/apache/zookeeper/NettyOptionalArchTest.java
b/zookeeper-server/src/test/java/org/apache/zookeeper/NettyOptionalArchTest.java
new file mode 100644
index 000000000..b95050541
--- /dev/null
+++
b/zookeeper-server/src/test/java/org/apache/zookeeper/NettyOptionalArchTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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 org.apache.zookeeper;
+
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
+import com.tngtech.archunit.base.DescribedPredicate;
+import com.tngtech.archunit.core.domain.JavaClass;
+import com.tngtech.archunit.core.domain.JavaClasses;
+import com.tngtech.archunit.core.importer.ClassFileImporter;
+import com.tngtech.archunit.core.importer.ImportOption;
+import com.tngtech.archunit.lang.ArchRule;
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Architectural test to enforce that Netty is an optional dependency.
+ *
+ * <p>Only classes whose name contains "Netty" (e.g. {@code
NettyServerCnxnFactory},
+ * {@code ClientCnxnSocketNetty}, {@code ClientNettyX509Util}) and a small set
of
+ * explicitly allowed SSL/TLS utility classes ({@code UnifiedServerSocket})
may depend
+ * on {@code io.netty} packages. All other ZooKeeper classes must remain
Netty-free so
+ * that Netty can be an optional dependency for users who do not need SSL/TLS.
+ */
+public class NettyOptionalArchTest {
+
+ @Test
+ public void nonNettyClassesShouldNotDependOnNetty() {
+ JavaClasses importedClasses = new ClassFileImporter(
+ Collections.singletonList(new
ImportOption.DoNotIncludeTests()))
+ .importPackages("org.apache.zookeeper")
+ .that(new DescribedPredicate<JavaClass>("ZK Non-Netty
classes") {
+ @Override
+ public boolean test(JavaClass javaClass) {
+ // Exclude classes with "Netty" in their name (e.g.
NettyServerCnxnFactory,
+ // ClientCnxnSocketNetty, NettyServerCnxn, NettyUtils,
ClientNettyX509Util).
+ // Also exclude UnifiedServerSocket (and its inner
classes) which legitimately
+ // uses the Netty SSL API to detect SSL vs plain-text
connections.
+ String name = javaClass.getName();
+ return !name.contains("Netty")
+ && !name.contains("UnifiedServerSocket");
+ }
+ });
+
+ ArchRule rule = noClasses().should()
+ .dependOnClassesThat().resideInAnyPackage("io.netty..")
+
.orShould().dependOnClassesThat().haveSimpleNameContaining("Netty");
+
+ rule.check(importedClasses);
+ }
+}
diff --git
a/zookeeper-server/src/test/java/org/apache/zookeeper/cli/HexDumpOutputFormatterTest.java
b/zookeeper-server/src/test/java/org/apache/zookeeper/cli/HexDumpOutputFormatterTest.java
new file mode 100644
index 000000000..29e4e17b2
--- /dev/null
+++
b/zookeeper-server/src/test/java/org/apache/zookeeper/cli/HexDumpOutputFormatterTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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 org.apache.zookeeper.cli;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+
+public class HexDumpOutputFormatterTest {
+
+ @Test
+ public void testFormatOneReadableExample() {
+ byte[] data = ("Hello," + "\n" + "Zoo" + "\u0001" +
"Keep!").getBytes(StandardCharsets.UTF_8);
+ String lineSeparator = System.lineSeparator();
+
+ String expected = String.join(
+ lineSeparator,
+ " +-------------------------------------------------+",
+ " | 0 1 2 3 4 5 6 7 8 9 a b c d e f |",
+
"+--------+-------------------------------------------------+----------------+",
+ "|00000000| 48 65 6c 6c 6f 2c 0a 5a 6f 6f 01 4b 65 65 70 21
|Hello,.Zoo.Keep!|",
+
"+--------+-------------------------------------------------+----------------+");
+
+ assertEquals(expected, HexDumpOutputFormatter.INSTANCE.format(data));
+ }
+}
+
diff --git
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
index 9999e9fd6..494bf6c1e 100644
---
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
+++
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
@@ -739,7 +739,7 @@ public void
testCreateNettySslContextForClient_customSSLContextClass(
X509KeyType caKeyType, X509KeyType certKeyType, String
keyPassword, Integer paramIndex)
throws Exception {
init(caKeyType, certKeyType, keyPassword, paramIndex);
- try (ClientX509Util clientX509Util = new ClientX509Util()) {
+ try (ClientNettyX509Util clientX509Util = new ClientNettyX509Util()) {
ZKConfig zkConfig = new ZKConfig();
zkConfig.setProperty(clientX509Util.getSslContextSupplierClassProperty(),
SslContextSupplier.class.getName());
// Disable hostname verification so the JdkSslContext is not
wrapped in a DelegatingSslContext.
@@ -759,7 +759,7 @@ public void
testCreateNettySslContextForServer_customSSLContextClass(
X509KeyType caKeyType, X509KeyType certKeyType, String
keyPassword, Integer paramIndex)
throws Exception {
init(caKeyType, certKeyType, keyPassword, paramIndex);
- try (ClientX509Util clientX509Util = new ClientX509Util()) {
+ try (ClientNettyX509Util clientX509Util = new ClientNettyX509Util()) {
ZKConfig zkConfig = new ZKConfig();
zkConfig.setProperty(clientX509Util.getSslContextSupplierClassProperty(),
SslContextSupplier.class.getName());
// A supplied SSLContext carries its own key material, so no key
store must be required.
@@ -781,7 +781,7 @@ public void
testCreateNettySslContext_customSSLContextClassRejectsNonJdkProvider
X509KeyType caKeyType, X509KeyType certKeyType, String
keyPassword, Integer paramIndex)
throws Exception {
init(caKeyType, certKeyType, keyPassword, paramIndex);
- try (ClientX509Util clientX509Util = new ClientX509Util()) {
+ try (ClientNettyX509Util clientX509Util = new ClientNettyX509Util()) {
ZKConfig zkConfig = new ZKConfig();
zkConfig.setProperty(clientX509Util.getSslContextSupplierClassProperty(),
SslContextSupplier.class.getName());
zkConfig.setProperty(clientX509Util.getSslProviderProperty(),
"OPENSSL");
@@ -815,7 +815,7 @@ public void
testCreateSSLContext_hostnameVerificationNoCustomTrustStore(X509KeyT
// Verify client hostname too
System.setProperty(x509Util.getSslClientHostnameVerificationEnabledProperty(),
"true");
ZKConfig zkConfig = new ZKConfig();
- try (ClientX509Util clientX509Util = new ClientX509Util();) {
+ try (ClientNettyX509Util clientX509Util = new ClientNettyX509Util();) {
UnpooledByteBufAllocator byteBufAllocator = new
UnpooledByteBufAllocator(false);
SslContext clientContext =
clientX509Util.createNettySslContextForClient(zkConfig);
SSLEngine clientEngine = clientContext.newEngine(byteBufAllocator);
@@ -1075,13 +1075,13 @@ public void
testSeparateClientKeyStore_nettyClientUsesClientKeyStore(
System.setProperty(x509Util.getSslClientKeystorePasswdProperty(),
keyPassword);
System.setProperty(x509Util.getSslClientKeystoreTypeProperty(), "JKS");
- try {
+ try (ClientNettyX509Util nettyX509Util = new ClientNettyX509Util()) {
// Netty client context should load from clientKeyStore
- SslContext clientCtx = ((ClientX509Util)
x509Util).createNettySslContextForClient(new ZKConfig());
+ SslContext clientCtx =
nettyX509Util.createNettySslContextForClient(new ZKConfig());
assertNotNull(clientCtx);
// Netty server context should still load from keyStore (the main
one)
- SslContext serverCtx = ((ClientX509Util)
x509Util).createNettySslContextForServer(new ZKConfig());
+ SslContext serverCtx =
nettyX509Util.createNettySslContextForServer(new ZKConfig());
assertNotNull(serverCtx);
} finally {
System.clearProperty(x509Util.getSslClientKeystoreLocationProperty());