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.junit.Assert.assertFalse;
021import static org.junit.Assert.assertNull;
022import static org.junit.Assert.fail;
023
024import java.net.HttpURLConnection;
025import java.net.URL;
026import java.util.ArrayList;
027import java.util.List;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.hbase.HBaseClassTestRule;
030import org.apache.hadoop.hbase.HBaseTestingUtility;
031import org.apache.hadoop.hbase.HConstants;
032import org.apache.hadoop.hbase.testclassification.ClientTests;
033import org.apache.hadoop.hbase.testclassification.LargeTests;
034import org.apache.hadoop.hbase.thrift.generated.Hbase;
035import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
036import org.apache.hadoop.hbase.util.EnvironmentEdgeManagerTestHelper;
037import org.apache.hadoop.hbase.util.IncrementingEnvironmentEdge;
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.AfterClass;
043import org.junit.Assert;
044import org.junit.BeforeClass;
045import org.junit.ClassRule;
046import org.junit.Rule;
047import org.junit.Test;
048import org.junit.experimental.categories.Category;
049import org.junit.rules.ExpectedException;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053import org.apache.hbase.thirdparty.com.google.common.base.Joiner;
054
055/**
056 * Start the HBase Thrift HTTP server on a random port through the command-line
057 * interface and talk to it from client side.
058 */
059@Category({ClientTests.class, LargeTests.class})
060public class TestThriftHttpServer {
061  @ClassRule
062  public static final HBaseClassTestRule CLASS_RULE =
063      HBaseClassTestRule.forClass(TestThriftHttpServer.class);
064
065  private static final Logger LOG =
066      LoggerFactory.getLogger(TestThriftHttpServer.class);
067
068  static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
069
070  private Thread httpServerThread;
071  private volatile Exception httpServerException;
072
073  private Exception clientSideException;
074
075  private ThriftServer thriftServer;
076  int port;
077
078  @BeforeClass
079  public static void setUpBeforeClass() throws Exception {
080    TEST_UTIL.getConfiguration().setBoolean("hbase.regionserver.thrift.http", true);
081    TEST_UTIL.getConfiguration().setBoolean("hbase.table.sanity.checks", false);
082    TEST_UTIL.startMiniCluster();
083    //ensure that server time increments every time we do an operation, otherwise
084    //successive puts having the same timestamp will override each other
085    EnvironmentEdgeManagerTestHelper.injectEdge(new IncrementingEnvironmentEdge());
086  }
087
088  @AfterClass
089  public static void tearDownAfterClass() throws Exception {
090    TEST_UTIL.shutdownMiniCluster();
091    EnvironmentEdgeManager.reset();
092  }
093
094  @Test
095  public void testExceptionThrownWhenMisConfigured() throws Exception {
096    Configuration conf = new Configuration(TEST_UTIL.getConfiguration());
097    conf.set("hbase.thrift.security.qop", "privacy");
098    conf.setBoolean("hbase.thrift.ssl.enabled", false);
099
100    ThriftServerRunner runner = null;
101    ExpectedException thrown = ExpectedException.none();
102    try {
103      thrown.expect(IllegalArgumentException.class);
104      thrown.expectMessage("Thrift HTTP Server's QoP is privacy, " +
105          "but hbase.thrift.ssl.enabled is false");
106      runner = new ThriftServerRunner(conf);
107      fail("Thrift HTTP Server starts up even with wrong security configurations.");
108    } catch (Exception e) {
109    }
110
111    assertNull(runner);
112  }
113
114  private void startHttpServerThread(final String[] args) {
115    LOG.info("Starting HBase Thrift server with HTTP server: " + Joiner.on(" ").join(args));
116
117    httpServerException = null;
118    httpServerThread = new Thread(() -> {
119      try {
120        thriftServer.doMain(args);
121      } catch (Exception e) {
122        httpServerException = e;
123      }
124    });
125    httpServerThread.setName(ThriftServer.class.getSimpleName() + "-httpServer");
126    httpServerThread.start();
127  }
128
129  @Rule
130  public ExpectedException exception = ExpectedException.none();
131
132  @Test
133  public void testRunThriftServerWithHeaderBufferLength() throws Exception {
134
135    // Test thrift server with HTTP header length less than 64k
136    try {
137      runThriftServer(1024 * 63);
138    } catch (TTransportException tex) {
139      assertFalse(tex.getMessage().equals("HTTP Response code: 431"));
140    }
141
142    // Test thrift server with HTTP header length more than 64k, expect an exception
143    exception.expect(TTransportException.class);
144    exception.expectMessage("HTTP Response code: 431");
145    runThriftServer(1024 * 64);
146  }
147
148  @Test
149  public void testRunThriftServer() throws Exception {
150    runThriftServer(0);
151  }
152
153  void runThriftServer(int customHeaderSize) throws Exception {
154    List<String> args = new ArrayList<>(3);
155    port = HBaseTestingUtility.randomFreePort();
156    args.add("-" + ThriftServer.PORT_OPTION);
157    args.add(String.valueOf(port));
158    args.add("-" + ThriftServer.INFOPORT_OPTION);
159    int infoPort = HBaseTestingUtility.randomFreePort();
160    args.add(String.valueOf(infoPort));
161    args.add("start");
162
163    thriftServer = new ThriftServer(TEST_UTIL.getConfiguration());
164    startHttpServerThread(args.toArray(new String[args.size()]));
165
166    // wait up to 10s for the server to start
167    HBaseTestingUtility.waitForHostPort(HConstants.LOCALHOST, port);
168
169    String url = "http://" + HConstants.LOCALHOST + ":" + port;
170    try {
171      checkHttpMethods(url);
172      talkToThriftServer(url, customHeaderSize);
173    } catch (Exception ex) {
174      clientSideException = ex;
175    } finally {
176      stopHttpServerThread();
177    }
178
179    if (clientSideException != null) {
180      LOG.error("Thrift client threw an exception " + clientSideException);
181      if (clientSideException instanceof TTransportException) {
182        throw clientSideException;
183      } else {
184        throw new Exception(clientSideException);
185      }
186    }
187  }
188
189  private void checkHttpMethods(String url) throws Exception {
190    // HTTP TRACE method should be disabled for security
191    // See https://www.owasp.org/index.php/Cross_Site_Tracing
192    HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
193    conn.setRequestMethod("TRACE");
194    conn.connect();
195    Assert.assertEquals(HttpURLConnection.HTTP_FORBIDDEN, conn.getResponseCode());
196  }
197
198  static volatile boolean tableCreated = false;
199
200  void talkToThriftServer(String url, int customHeaderSize) throws Exception {
201    THttpClient httpClient = new THttpClient(url);
202    httpClient.open();
203
204    if (customHeaderSize > 0) {
205      StringBuilder sb = new StringBuilder();
206      for (int i = 0; i < customHeaderSize; i++) {
207        sb.append("a");
208      }
209      httpClient.setCustomHeader("User-Agent", sb.toString());
210    }
211
212    try {
213      TProtocol prot;
214      prot = new TBinaryProtocol(httpClient);
215      Hbase.Client client = new Hbase.Client(prot);
216      if (!tableCreated){
217        TestThriftServer.createTestTables(client);
218        tableCreated = true;
219      }
220      TestThriftServer.checkTableList(client);
221    } finally {
222      httpClient.close();
223    }
224  }
225
226  private void stopHttpServerThread() throws Exception {
227    LOG.debug("Stopping " + " Thrift HTTP server");
228    thriftServer.stop();
229    httpServerThread.join();
230    if (httpServerException != null) {
231      LOG.error("Command-line invocation of HBase Thrift server threw an " +
232          "exception", httpServerException);
233      throw new Exception(httpServerException);
234    }
235  }
236}