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