This is an automated email from the ASF dual-hosted git repository. kezhuw 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 75f9781ec ZOOKEEPER-4819: Fix can't seek for writable tls server if connected to readonly server 75f9781ec is described below commit 75f9781ecc3cde9fa5a032fa8cfa8c79b69ef879 Author: Xin Luo <65529035+luoxi...@users.noreply.github.com> AuthorDate: Wed Nov 27 09:21:45 2024 +0800 ZOOKEEPER-4819: Fix can't seek for writable tls server if connected to readonly server Reviewers: kezhuw Author: luoxiner Closes #2200 from luoxiner/ZOOKEEPER-4819 --- .../main/java/org/apache/zookeeper/ClientCnxn.java | 37 +----- .../zookeeper/client/FourLetterWordMain.java | 29 +++++ .../zookeeper/test/ReadOnlyModeWithSSLTest.java | 137 +++++++++++++++++++++ 3 files changed, 171 insertions(+), 32 deletions(-) diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxn.java b/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxn.java index b23ce82dc..c24308fb6 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxn.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxn.java @@ -19,13 +19,10 @@ package org.apache.zookeeper; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.net.ConnectException; import java.net.InetSocketAddress; -import java.net.Socket; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.util.ArrayDeque; @@ -67,10 +64,12 @@ import org.apache.zookeeper.Watcher.Event.KeeperState; import org.apache.zookeeper.ZooDefs.OpCode; import org.apache.zookeeper.ZooKeeper.States; import org.apache.zookeeper.ZooKeeper.WatchRegistration; +import org.apache.zookeeper.client.FourLetterWordMain; import org.apache.zookeeper.client.HostProvider; import org.apache.zookeeper.client.ZKClientConfig; import org.apache.zookeeper.client.ZooKeeperSaslClient; import org.apache.zookeeper.common.Time; +import org.apache.zookeeper.common.X509Exception; import org.apache.zookeeper.proto.AuthPacket; import org.apache.zookeeper.proto.ConnectRequest; import org.apache.zookeeper.proto.Create2Response; @@ -1303,42 +1302,16 @@ public class ClientCnxn { InetSocketAddress addr = hostProvider.next(0); LOG.info("Checking server {} for being r/w. Timeout {}", addr, pingRwTimeout); - - Socket sock = null; - BufferedReader br = null; try { - sock = new Socket(addr.getHostString(), addr.getPort()); - sock.setSoLinger(false, -1); - sock.setSoTimeout(1000); - sock.setTcpNoDelay(true); - sock.getOutputStream().write("isro".getBytes()); - sock.getOutputStream().flush(); - sock.shutdownOutput(); - br = new BufferedReader(new InputStreamReader(sock.getInputStream())); - result = br.readLine(); + result = FourLetterWordMain.send4LetterWord(addr.getHostString(), addr.getPort(), "isro", clientConfig, 1000); } catch (ConnectException e) { // ignore, this just means server is not up - } catch (IOException e) { + } catch (IOException | X509Exception.SSLContextException e) { // some unexpected error, warn about it LOG.warn("Exception while seeking for r/w server.", e); - } finally { - if (sock != null) { - try { - sock.close(); - } catch (IOException e) { - LOG.warn("Unexpected exception", e); - } - } - if (br != null) { - try { - br.close(); - } catch (IOException e) { - LOG.warn("Unexpected exception", e); - } - } } - if ("rw".equals(result)) { + if ("rw\n".equals(result)) { pingRwTimeout = minPingRwTimeout; // save the found address so that it's used during the next // connection attempt diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/client/FourLetterWordMain.java b/zookeeper-server/src/main/java/org/apache/zookeeper/client/FourLetterWordMain.java index 39988c39b..b4e660287 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/client/FourLetterWordMain.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/client/FourLetterWordMain.java @@ -94,6 +94,33 @@ public class FourLetterWordMain { return send4LetterWord(host, port, cmd, secure, timeout, null); } + /** + * Send the 4letterword + * @param host the destination host + * @param port the destination port + * @param cmd the 4letterword + * @param clientConfig client config + * @param timeout in milliseconds, maximum time to wait while connecting/reading data + * @return server response + * @throws SSLContextException + * @throws IOException + */ + public static String send4LetterWord( + String host, + int port, + String cmd, + ZKClientConfig clientConfig, + int timeout) throws SSLContextException, IOException { + boolean useSecure = clientConfig.getBoolean(ZKClientConfig.SECURE_CLIENT); + SSLContext sslContext = null; + if (useSecure) { + try (X509Util x509Util = new ClientX509Util()) { + sslContext = x509Util.createSSLContext(clientConfig); + } + } + return send4LetterWord(host, port, cmd, useSecure, timeout, sslContext); + } + /** * Send the 4letterword * @param host the destination host @@ -137,7 +164,9 @@ public class FourLetterWordMain { sock = new Socket(); sock.connect(hostaddress, timeout); } + sock.setSoLinger(false, -1); sock.setSoTimeout(timeout); + sock.setTcpNoDelay(true); OutputStream outstream = sock.getOutputStream(); outstream.write(cmd.getBytes(UTF_8)); outstream.flush(); diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/test/ReadOnlyModeWithSSLTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/test/ReadOnlyModeWithSSLTest.java new file mode 100644 index 000000000..701cd0f6d --- /dev/null +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/test/ReadOnlyModeWithSSLTest.java @@ -0,0 +1,137 @@ +/* + * 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.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayOutputStream; +import java.io.LineNumberReader; +import java.io.StringReader; +import java.util.regex.Pattern; +import org.apache.zookeeper.CreateMode; +import org.apache.zookeeper.ZKTestCase; +import org.apache.zookeeper.ZooDefs; +import org.apache.zookeeper.ZooKeeper; +import org.apache.zookeeper.client.ZKClientConfig; +import org.apache.zookeeper.common.ClientX509Util; +import org.apache.zookeeper.common.StringUtils; +import org.apache.zookeeper.server.NettyServerCnxnFactory; +import org.apache.zookeeper.server.ServerCnxnFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +public class ReadOnlyModeWithSSLTest extends ZKTestCase { + + private static int CONNECTION_TIMEOUT = QuorumBase.CONNECTION_TIMEOUT; + private QuorumUtil qu = new QuorumUtil(1); + private ClientX509Util clientX509Util; + private ZKClientConfig clientConfig; + + @BeforeEach + public void setUp() throws Exception { + clientX509Util = new ClientX509Util(); + String testDataPath = System.getProperty("test.data.dir", "src/test/resources/data"); + clientConfig = new ZKClientConfig(); + clientConfig.setProperty(ZKClientConfig.SECURE_CLIENT, "true"); + clientConfig.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET, "org.apache.zookeeper.ClientCnxnSocketNetty"); + clientConfig.setProperty(clientX509Util.getSslKeystoreLocationProperty(), testDataPath + "/ssl/testKeyStore.jks"); + clientConfig.setProperty(clientX509Util.getSslKeystorePasswdProperty(), "testpass"); + clientConfig.setProperty(clientX509Util.getSslTruststoreLocationProperty(), testDataPath + "/ssl/testTrustStore.jks"); + clientConfig.setProperty(clientX509Util.getSslTruststorePasswdProperty(), "testpass"); + System.setProperty("readonlymode.enabled", "true"); + System.setProperty(NettyServerCnxnFactory.PORT_UNIFICATION_KEY, Boolean.TRUE.toString()); + System.setProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY, "org.apache.zookeeper.server.NettyServerCnxnFactory"); + System.setProperty(clientX509Util.getSslKeystoreLocationProperty(), testDataPath + "/ssl/testKeyStore.jks"); + System.setProperty(clientX509Util.getSslKeystorePasswdProperty(), "testpass"); + System.setProperty(clientX509Util.getSslTruststoreLocationProperty(), testDataPath + "/ssl/testTrustStore.jks"); + System.setProperty(clientX509Util.getSslTruststorePasswdProperty(), "testpass"); + } + + @AfterEach + public void tearDown() throws Exception { + System.clearProperty(NettyServerCnxnFactory.PORT_UNIFICATION_KEY); + System.clearProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY); + System.clearProperty(clientX509Util.getSslKeystoreLocationProperty()); + System.clearProperty(clientX509Util.getSslKeystorePasswdProperty()); + System.clearProperty(clientX509Util.getSslKeystorePasswdPathProperty()); + System.clearProperty(clientX509Util.getSslTruststoreLocationProperty()); + System.clearProperty(clientX509Util.getSslTruststorePasswdProperty()); + System.clearProperty(clientX509Util.getSslTruststorePasswdPathProperty()); + System.clearProperty(clientX509Util.getFipsModeProperty()); + System.clearProperty(clientX509Util.getSslHostnameVerificationEnabledProperty()); + System.clearProperty(clientX509Util.getSslProviderProperty()); + clientX509Util.close(); + System.setProperty("readonlymode.enabled", "false"); + qu.tearDown(); + } + + /** + * Ensures that client seeks for r/w servers while it's connected to r/o + * server. + */ + @Test + @Timeout(value = 90) + public void testSeekForRwServerWithSSL() throws Exception { + qu.enableLocalSession(true); + qu.startQuorum(); + + try (LoggerTestTool loggerTestTool = new LoggerTestTool("org.apache.zookeeper")) { + ByteArrayOutputStream os = loggerTestTool.getOutputStream(); + + qu.shutdown(2); + ClientBase.CountdownWatcher watcher = new ClientBase.CountdownWatcher(); + ZooKeeper zk = new ZooKeeper(qu.getConnString(), CONNECTION_TIMEOUT, watcher, true, clientConfig); + watcher.waitForConnected(CONNECTION_TIMEOUT); + + // if we don't suspend a peer it will rejoin a quorum + qu.getPeer(1).peer + .setSuspended(true); + + // start two servers to form a quorum; client should detect this and + // connect to one of them + watcher.reset(); + qu.start(2); + qu.start(3); + ClientBase.waitForServerUp(qu.getConnString(), 2000); + watcher.waitForConnected(CONNECTION_TIMEOUT); + zk.create("/test", "test".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); + + // resume poor fellow + qu.getPeer(1).peer + .setSuspended(false); + + String log = os.toString(); + assertFalse(StringUtils.isEmpty(log), "OutputStream doesn't have any log messages"); + + LineNumberReader r = new LineNumberReader(new StringReader(log)); + String line; + Pattern p = Pattern.compile(".*Majority server found.*"); + boolean found = false; + while ((line = r.readLine()) != null) { + if (p.matcher(line).matches()) { + found = true; + break; + } + } + assertTrue(found, "Majority server wasn't found while connected to r/o server"); + } + } +}