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.Assert.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.HBaseClassTestRule;
038import org.apache.hadoop.hbase.HBaseConfiguration;
039import org.apache.hadoop.hbase.Waiter;
040import org.apache.hadoop.hbase.client.MetricsConnection.CallStats;
041import org.apache.hadoop.hbase.io.crypto.tls.X509Util;
042import org.apache.hadoop.hbase.net.Address;
043import org.apache.hadoop.hbase.security.User;
044import org.apache.hadoop.hbase.testclassification.ClientTests;
045import org.apache.hadoop.hbase.testclassification.SmallTests;
046import org.junit.After;
047import org.junit.Before;
048import org.junit.ClassRule;
049import org.junit.Test;
050import org.junit.experimental.categories.Category;
051import org.mockito.invocation.InvocationOnMock;
052import org.mockito.stubbing.Answer;
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055
056import org.apache.hbase.thirdparty.com.google.common.io.Closeables;
057import org.apache.hbase.thirdparty.io.netty.handler.ssl.NotSslRecordException;
058
059import org.apache.hadoop.hbase.shaded.protobuf.generated.RegistryProtos.ClientMetaService;
060
061/**
062 * A simple UT to make sure that we do not leak the SslExceptions to netty's TailContext, where it
063 * will generate a confusing WARN message.
064 * <p>
065 * See HBASE-27782 for more details.
066 */
067@Category({ ClientTests.class, SmallTests.class })
068public class TestTLSHandshadeFailure {
069
070  @ClassRule
071  public static final HBaseClassTestRule CLASS_RULE =
072    HBaseClassTestRule.forClass(TestTLSHandshadeFailure.class);
073
074  private static final Logger LOG = LoggerFactory.getLogger(TestTLSHandshadeFailure.class);
075
076  private final Configuration conf = HBaseConfiguration.create();
077
078  // use a pre set seed to make the random bytes stable
079  private final Random rand = new Random(1);
080
081  private ServerSocket server;
082
083  private Thread serverThread;
084
085  private NettyRpcClient client;
086
087  private org.apache.logging.log4j.core.Appender mockAppender;
088
089  private void serve() {
090    Socket socket = null;
091    try {
092      socket = server.accept();
093      byte[] bytes = new byte[128];
094      rand.nextBytes(bytes);
095      socket.getOutputStream().write(bytes);
096      socket.getOutputStream().flush();
097    } catch (Exception e) {
098      LOG.warn("failed to process request", e);
099    } finally {
100      if (socket != null) {
101        try {
102          socket.close();
103        } catch (IOException e1) {
104          LOG.warn("failed to close socket");
105        }
106      }
107    }
108  }
109
110  @Before
111  public void setUp() throws IOException {
112    server = new ServerSocket(0);
113    serverThread = new Thread(this::serve);
114    serverThread.setDaemon(true);
115    serverThread.setName("Error-Server-Thread");
116    serverThread.start();
117    conf.setBoolean(X509Util.HBASE_CLIENT_NETTY_TLS_ENABLED, true);
118    client = new NettyRpcClient(conf);
119
120    mockAppender = mock(org.apache.logging.log4j.core.Appender.class);
121    when(mockAppender.getName()).thenReturn("mockAppender");
122    when(mockAppender.isStarted()).thenReturn(true);
123    ((org.apache.logging.log4j.core.Logger) org.apache.logging.log4j.LogManager
124      .getLogger(BufferCallBeforeInitHandler.class)).addAppender(mockAppender);
125  }
126
127  @After
128  public void tearDown() throws IOException {
129    ((org.apache.logging.log4j.core.Logger) org.apache.logging.log4j.LogManager
130      .getLogger(BufferCallBeforeInitHandler.class)).removeAppender(mockAppender);
131    Closeables.close(client, true);
132    Closeables.close(server, true);
133  }
134
135  @Test
136  public void test() throws Exception {
137    AtomicReference<org.apache.logging.log4j.Level> level = new AtomicReference<>();
138    AtomicReference<String> msg = new AtomicReference<String>();
139    doAnswer(new Answer<Void>() {
140
141      @Override
142      public Void answer(InvocationOnMock invocation) throws Throwable {
143        org.apache.logging.log4j.core.LogEvent logEvent =
144          invocation.getArgument(0, org.apache.logging.log4j.core.LogEvent.class);
145        level.set(logEvent.getLevel());
146        msg.set(logEvent.getMessage().getFormattedMessage());
147        return null;
148      }
149    }).when(mockAppender).append(any());
150    ConnectionId id = new ConnectionId(User.getCurrent(), "test",
151      Address.fromParts("127.0.0.1", server.getLocalPort()));
152    NettyRpcConnection conn = client.createConnection(id);
153    BlockingRpcCallback<Call> done = new BlockingRpcCallback<>();
154    Call call = new Call(1, ClientMetaService.getDescriptor().getMethods().get(0), null, null, null,
155      0, 0, Collections.emptyMap(), done, new CallStats());
156    HBaseRpcController hrc = new HBaseRpcControllerImpl();
157    conn.sendRequest(call, hrc);
158    done.get();
159    call.error.printStackTrace();
160    assertThat(call.error, instanceOf(NotSslRecordException.class));
161    Waiter.waitFor(conf, 5000, () -> msg.get() != null);
162    verify(mockAppender).append(any());
163    // make sure that it has been logged by BufferCallBeforeInitHandler
164    assertEquals(org.apache.logging.log4j.Level.DEBUG, level.get());
165    assertThat(msg.get(),
166      startsWith("got ssl exception, which should have already been proceeded"));
167  }
168}