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.thrift;
019
020import static org.apache.hadoop.hbase.thrift.TestThriftServerCmdLine.createBoundServer;
021import static org.junit.jupiter.api.Assertions.assertEquals;
022import static org.junit.jupiter.api.Assertions.assertNotEquals;
023import static org.junit.jupiter.api.Assertions.assertThrows;
024import static org.junit.jupiter.api.Assertions.fail;
025
026import java.io.IOException;
027import java.net.HttpURLConnection;
028import java.net.URL;
029import java.util.function.Supplier;
030import org.apache.hadoop.conf.Configuration;
031import org.apache.hadoop.hbase.HBaseTestingUtil;
032import org.apache.hadoop.hbase.HConstants;
033import org.apache.hadoop.hbase.thrift.generated.Hbase;
034import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
035import org.apache.hadoop.hbase.util.EnvironmentEdgeManagerTestHelper;
036import org.apache.hadoop.hbase.util.IncrementingEnvironmentEdge;
037import org.apache.hadoop.hbase.util.TableDescriptorChecker;
038import org.apache.thrift.protocol.TBinaryProtocol;
039import org.apache.thrift.protocol.TProtocol;
040import org.apache.thrift.transport.THttpClient;
041import org.apache.thrift.transport.TTransportException;
042import org.junit.jupiter.api.Test;
043import org.junit.rules.ExpectedException;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047/**
048 * Base class for testing HBase Thrift HTTP server. Shared logic without @BeforeAll/@AfterAll to
049 * allow subclasses to manage their own lifecycle.
050 */
051public class TestThriftHttpServerBase {
052
053  private static final Logger LOG = LoggerFactory.getLogger(TestThriftHttpServerBase.class);
054
055  protected static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
056
057  public static void setUpBeforeClass() throws Exception {
058    TEST_UTIL.getConfiguration().setBoolean(Constants.USE_HTTP_CONF_KEY, true);
059    TEST_UTIL.getConfiguration().setBoolean(TableDescriptorChecker.TABLE_SANITY_CHECKS, false);
060    TEST_UTIL.startMiniCluster();
061    // ensure that server time increments every time we do an operation, otherwise
062    // successive puts having the same timestamp will override each other
063    EnvironmentEdgeManagerTestHelper.injectEdge(new IncrementingEnvironmentEdge());
064  }
065
066  public static void tearDownAfterClass() throws Exception {
067    TEST_UTIL.shutdownMiniCluster();
068    EnvironmentEdgeManager.reset();
069  }
070
071  @Test
072  public void testExceptionThrownWhenMisConfigured() throws IOException {
073    Configuration conf = new Configuration(TEST_UTIL.getConfiguration());
074    conf.set("hbase.thrift.security.qop", "privacy");
075    conf.setBoolean("hbase.thrift.ssl.enabled", false);
076    ExpectedException thrown = ExpectedException.none();
077    ThriftServerRunner tsr = null;
078    try {
079      thrown.expect(IllegalArgumentException.class);
080      thrown.expectMessage(
081        "Thrift HTTP Server's QoP is privacy, " + "but hbase.thrift.ssl.enabled is false");
082      tsr = TestThriftServerCmdLine.createBoundServer(() -> new ThriftServer(conf));
083      fail("Thrift HTTP Server starts up even with wrong security configurations.");
084    } catch (Exception e) {
085      LOG.info("Expected!", e);
086    } finally {
087      if (tsr != null) {
088        tsr.close();
089      }
090    }
091  }
092
093  @Test
094  public void testRunThriftServerWithHeaderBufferLength() throws Exception {
095    // Test thrift server with HTTP header length less than 64k
096    try {
097      runThriftServer(1024 * 63);
098    } catch (TTransportException tex) {
099      assertNotEquals("HTTP Response code: 431", tex.getMessage());
100    }
101
102    // Test thrift server with HTTP header length more than 64k, expect an exception
103    TTransportException exception =
104      assertThrows(TTransportException.class, () -> runThriftServer(1024 * 64));
105    assertEquals("HTTP Response code: 431", exception.getMessage());
106  }
107
108  protected Supplier<ThriftServer> getThriftServerSupplier() {
109    return () -> new ThriftServer(TEST_UTIL.getConfiguration());
110  }
111
112  @Test
113  public void testRunThriftServer() throws Exception {
114    runThriftServer(0);
115  }
116
117  void runThriftServer(int customHeaderSize) throws Exception {
118    // Add retries in case we see stuff like connection reset
119    Exception clientSideException = null;
120    for (int i = 0; i < 10; i++) {
121      clientSideException = null;
122      ThriftServerRunner tsr = createBoundServer(getThriftServerSupplier());
123      String url = "http://" + HConstants.LOCALHOST + ":" + tsr.getThriftServer().listenPort;
124      try {
125        checkHttpMethods(url);
126        talkToThriftServer(url, customHeaderSize);
127        break;
128      } catch (Exception ex) {
129        clientSideException = ex;
130        LOG.info("Client-side Exception", ex);
131      } finally {
132        tsr.close();
133        tsr.join();
134        if (tsr.getRunException() != null) {
135          LOG.error("Invocation of HBase Thrift server threw exception", tsr.getRunException());
136          throw tsr.getRunException();
137        }
138      }
139    }
140
141    if (clientSideException != null) {
142      LOG.error("Thrift Client", clientSideException);
143      throw clientSideException;
144    }
145  }
146
147  private void checkHttpMethods(String url) throws Exception {
148    // HTTP TRACE method should be disabled for security
149    // See https://www.owasp.org/index.php/Cross_Site_Tracing
150    HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
151    conn.setRequestMethod("TRACE");
152    conn.connect();
153    assertEquals(HttpURLConnection.HTTP_FORBIDDEN, conn.getResponseCode(),
154      conn.getResponseMessage());
155  }
156
157  protected static volatile boolean tableCreated = false;
158
159  protected void talkToThriftServer(String url, int customHeaderSize) throws Exception {
160    THttpClient httpClient = new THttpClient(url);
161    httpClient.open();
162
163    if (customHeaderSize > 0) {
164      StringBuilder sb = new StringBuilder();
165      for (int i = 0; i < customHeaderSize; i++) {
166        sb.append("a");
167      }
168      httpClient.setCustomHeader("User-Agent", sb.toString());
169    }
170
171    try {
172      TProtocol prot;
173      prot = new TBinaryProtocol(httpClient);
174      Hbase.Client client = new Hbase.Client(prot);
175      if (!tableCreated) {
176        TestThriftServer.createTestTables(client);
177        tableCreated = true;
178      }
179      TestThriftServer.checkTableList(client);
180    } finally {
181      httpClient.close();
182    }
183  }
184}