001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.ipc;
019
020import static org.hamcrest.MatcherAssert.assertThat;
021import static org.hamcrest.Matchers.instanceOf;
022import static org.hamcrest.Matchers.startsWith;
023import static org.junit.jupiter.api.Assertions.assertEquals;
024import static org.mockito.ArgumentMatchers.any;
025import static org.mockito.Mockito.doAnswer;
026import static org.mockito.Mockito.mock;
027import static org.mockito.Mockito.verify;
028import static org.mockito.Mockito.when;
029
030import java.io.IOException;
031import java.net.ServerSocket;
032import java.net.Socket;
033import java.util.Collections;
034import java.util.Random;
035import java.util.concurrent.atomic.AtomicReference;
036import org.apache.hadoop.conf.Configuration;
037import org.apache.hadoop.hbase.HBaseConfiguration;
038import org.apache.hadoop.hbase.Waiter;
039import org.apache.hadoop.hbase.client.MetricsConnection.CallStats;
040import org.apache.hadoop.hbase.io.crypto.tls.X509Util;
041import org.apache.hadoop.hbase.net.Address;
042import org.apache.hadoop.hbase.security.User;
043import org.apache.hadoop.hbase.testclassification.ClientTests;
044import org.apache.hadoop.hbase.testclassification.SmallTests;
045import org.junit.jupiter.api.AfterEach;
046import org.junit.jupiter.api.BeforeEach;
047import org.junit.jupiter.api.Tag;
048import org.junit.jupiter.api.Test;
049import org.mockito.invocation.InvocationOnMock;
050import org.mockito.stubbing.Answer;
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054import org.apache.hbase.thirdparty.com.google.common.io.Closeables;
055import org.apache.hbase.thirdparty.io.netty.handler.ssl.NotSslRecordException;
056
057import org.apache.hadoop.hbase.shaded.protobuf.generated.RegistryProtos.ClientMetaService;
058
059/**
060 * A simple UT to make sure that we do not leak the SslExceptions to netty's TailContext, where it
061 * will generate a confusing WARN message.
062 * <p>
063 * See HBASE-27782 for more details.
064 */
065@Tag(ClientTests.TAG)
066@Tag(SmallTests.TAG)
067public class TestTLSHandshadeFailure {
068
069  private static final Logger LOG = LoggerFactory.getLogger(TestTLSHandshadeFailure.class);
070
071  private final Configuration conf = HBaseConfiguration.create();
072
073  // use a pre set seed to make the random bytes stable
074  private final Random rand = new Random(1);
075
076  private ServerSocket server;
077
078  private Thread serverThread;
079
080  private NettyRpcClient client;
081
082  private org.apache.logging.log4j.core.Appender mockAppender;
083
084  private void serve() {
085    Socket socket = null;
086    try {
087      socket = server.accept();
088      byte[] bytes = new byte[128];
089      rand.nextBytes(bytes);
090      socket.getOutputStream().write(bytes);
091      socket.getOutputStream().flush();
092    } catch (Exception e) {
093      LOG.warn("failed to process request", e);
094    } finally {
095      if (socket != null) {
096        try {
097          socket.close();
098        } catch (IOException e1) {
099          LOG.warn("failed to close socket");
100        }
101      }
102    }
103  }
104
105  @BeforeEach
106  public void setUp() throws IOException {
107    server = new ServerSocket(0);
108    serverThread = new Thread(this::serve);
109    serverThread.setDaemon(true);
110    serverThread.setName("Error-Server-Thread");
111    serverThread.start();
112    conf.setBoolean(X509Util.HBASE_CLIENT_NETTY_TLS_ENABLED, true);
113    client = new NettyRpcClient(conf);
114
115    mockAppender = mock(org.apache.logging.log4j.core.Appender.class);
116    when(mockAppender.getName()).thenReturn("mockAppender");
117    when(mockAppender.isStarted()).thenReturn(true);
118    ((org.apache.logging.log4j.core.Logger) org.apache.logging.log4j.LogManager
119      .getLogger(BufferCallBeforeInitHandler.class)).addAppender(mockAppender);
120  }
121
122  @AfterEach
123  public void tearDown() throws IOException {
124    ((org.apache.logging.log4j.core.Logger) org.apache.logging.log4j.LogManager
125      .getLogger(BufferCallBeforeInitHandler.class)).removeAppender(mockAppender);
126    Closeables.close(client, true);
127    Closeables.close(server, true);
128  }
129
130  @Test
131  public void test() throws Exception {
132    AtomicReference<org.apache.logging.log4j.Level> level = new AtomicReference<>();
133    AtomicReference<String> msg = new AtomicReference<String>();
134    doAnswer(new Answer<Void>() {
135
136      @Override
137      public Void answer(InvocationOnMock invocation) throws Throwable {
138        org.apache.logging.log4j.core.LogEvent logEvent =
139          invocation.getArgument(0, org.apache.logging.log4j.core.LogEvent.class);
140        level.set(logEvent.getLevel());
141        msg.set(logEvent.getMessage().getFormattedMessage());
142        return null;
143      }
144    }).when(mockAppender).append(any());
145    ConnectionId id = new ConnectionId(User.getCurrent(), "test",
146      Address.fromParts("127.0.0.1", server.getLocalPort()));
147    NettyRpcConnection conn = client.createConnection(id);
148    BlockingRpcCallback<Call> done = new BlockingRpcCallback<>();
149    Call call = new Call(1, ClientMetaService.getDescriptor().getMethods().get(0), null, null, null,
150      0, 0, Collections.emptyMap(), done, new CallStats());
151    HBaseRpcController hrc = new HBaseRpcControllerImpl();
152    conn.sendRequest(call, hrc);
153    done.get();
154    call.error.printStackTrace();
155    assertThat(call.error, instanceOf(NotSslRecordException.class));
156    Waiter.waitFor(conf, 5000, () -> msg.get() != null);
157    verify(mockAppender).append(any());
158    // make sure that it has been logged by BufferCallBeforeInitHandler
159    assertEquals(org.apache.logging.log4j.Level.DEBUG, level.get());
160    assertThat(msg.get(),
161      startsWith("got ssl exception, which should have already been proceeded"));
162  }
163}