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