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.
+ *
+ * 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.
+ *
+ *
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 ? toNettyClientAuth(X509Util.ClientAuth.NONE) : toNettyClientAuth(getClientAuth(config)),
+ 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 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 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/ClientX509Util.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientX509Util.java
index 82b1bb3fd1e..473aab88047 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.
+ *
+ * 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 = buildKeyManager(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 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, buildTrustManager(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 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.
- *
- * 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.
- *
- *
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 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 2a91e87d217..07186be71f0 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";
@@ -326,7 +316,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 6f65787442c..061caf85cae 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;
@@ -112,7 +113,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;
@@ -121,7 +122,7 @@ public void setOutstandingHandshakeLimit(int limit) {
private static final AtomicReference 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
@@ -511,7 +512,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 f63c1eec4ba..32f036eb0f9 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 080f4f8638d..511cdd5838e 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
@@ -2379,8 +2379,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 c721e685bb6..0545827c893 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 56dcbd224d1..dd599d9a997 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 86b3b8a7c6a..3fb3d6bf313 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 00000000000..b95050541ef
--- /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.
+ *
+ * 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("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 00000000000..29e4e17b257
--- /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 f76b91ddf9f..fdeec8872de 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
@@ -734,7 +734,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.
@@ -754,7 +754,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.
@@ -776,7 +776,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");
@@ -810,7 +810,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);