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.Assert.assertEquals;
022
023import java.io.BufferedInputStream;
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStream;
027import java.lang.reflect.Method;
028import java.net.HttpURLConnection;
029import java.nio.file.Files;
030import java.security.KeyPair;
031import java.security.KeyStore;
032import java.security.cert.X509Certificate;
033import javax.net.ssl.SSLContext;
034import org.apache.hadoop.conf.Configuration;
035import org.apache.hadoop.hbase.HBaseClassTestRule;
036import org.apache.hadoop.hbase.HBaseTestingUtil;
037import org.apache.hadoop.hbase.HConstants;
038import org.apache.hadoop.hbase.testclassification.ClientTests;
039import org.apache.hadoop.hbase.testclassification.LargeTests;
040import org.apache.hadoop.hbase.thrift.generated.Hbase;
041import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
042import org.apache.hadoop.hbase.util.EnvironmentEdgeManagerTestHelper;
043import org.apache.hadoop.hbase.util.IncrementingEnvironmentEdge;
044import org.apache.hadoop.hbase.util.TableDescriptorChecker;
045import org.apache.hadoop.security.ssl.KeyStoreTestUtil;
046import org.apache.http.client.methods.CloseableHttpResponse;
047import org.apache.http.client.methods.HttpPost;
048import org.apache.http.entity.ByteArrayEntity;
049import org.apache.http.impl.client.CloseableHttpClient;
050import org.apache.http.impl.client.HttpClientBuilder;
051import org.apache.http.impl.client.HttpClients;
052import org.apache.http.ssl.SSLContexts;
053import org.apache.thrift.protocol.TBinaryProtocol;
054import org.apache.thrift.protocol.TProtocol;
055import org.apache.thrift.transport.TMemoryBuffer;
056import org.junit.After;
057import org.junit.AfterClass;
058import org.junit.Before;
059import org.junit.BeforeClass;
060import org.junit.ClassRule;
061import org.junit.Test;
062import org.junit.experimental.categories.Category;
063import org.slf4j.Logger;
064import org.slf4j.LoggerFactory;
065
066@Category({ ClientTests.class, LargeTests.class })
067public class TestThriftHttpServerSSL {
068  @ClassRule
069  public static final HBaseClassTestRule CLASS_RULE =
070    HBaseClassTestRule.forClass(TestThriftHttpServerSSL.class);
071
072  private static final Logger LOG = LoggerFactory.getLogger(TestThriftHttpServerSSL.class);
073  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
074  private static final String KEY_STORE_PASSWORD = "myKSPassword";
075  private static final String TRUST_STORE_PASSWORD = "myTSPassword";
076
077  private File keyDir;
078  private HttpClientBuilder httpClientBuilder;
079  private ThriftServerRunner tsr;
080  private HttpPost httpPost = null;
081
082  @BeforeClass
083  public static void setUpBeforeClass() throws Exception {
084    TEST_UTIL.getConfiguration().setBoolean(Constants.USE_HTTP_CONF_KEY, true);
085    TEST_UTIL.getConfiguration().setBoolean(TableDescriptorChecker.TABLE_SANITY_CHECKS, false);
086    TEST_UTIL.startMiniCluster();
087    // ensure that server time increments every time we do an operation, otherwise
088    // successive puts having the same timestamp will override each other
089    EnvironmentEdgeManagerTestHelper.injectEdge(new IncrementingEnvironmentEdge());
090  }
091
092  @AfterClass
093  public static void tearDownAfterClass() throws Exception {
094    TEST_UTIL.shutdownMiniCluster();
095    EnvironmentEdgeManager.reset();
096  }
097
098  @Before
099  public void setUp() throws Exception {
100    initializeAlgorithmId();
101    keyDir = initKeystoreDir();
102    keyDir.deleteOnExit();
103    KeyPair keyPair = KeyStoreTestUtil.generateKeyPair("RSA");
104
105    X509Certificate serverCertificate =
106      KeyStoreTestUtil.generateCertificate("CN=localhost, O=server", keyPair, 30, "SHA1withRSA");
107
108    generateTrustStore(serverCertificate);
109    generateKeyStore(keyPair, serverCertificate);
110
111    Configuration conf = new Configuration(TEST_UTIL.getConfiguration());
112    conf.setBoolean(Constants.THRIFT_SSL_ENABLED_KEY, true);
113    conf.set(Constants.THRIFT_SSL_KEYSTORE_STORE_KEY, getKeystoreFilePath());
114    conf.set(Constants.THRIFT_SSL_KEYSTORE_PASSWORD_KEY, KEY_STORE_PASSWORD);
115    conf.set(Constants.THRIFT_SSL_KEYSTORE_KEYPASSWORD_KEY, KEY_STORE_PASSWORD);
116
117    tsr = createBoundServer(() -> new ThriftServer(conf));
118    String url = "https://" + HConstants.LOCALHOST + ":" + tsr.getThriftServer().listenPort;
119
120    KeyStore trustStore;
121    trustStore = KeyStore.getInstance("JKS");
122    try (InputStream inputStream =
123      new BufferedInputStream(Files.newInputStream(new File(getTruststoreFilePath()).toPath()))) {
124      trustStore.load(inputStream, TRUST_STORE_PASSWORD.toCharArray());
125    }
126
127    httpClientBuilder = HttpClients.custom();
128    SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(trustStore, null).build();
129    httpClientBuilder.setSSLContext(sslcontext);
130
131    httpPost = new HttpPost(url);
132    httpPost.setHeader("Content-Type", "application/x-thrift");
133    httpPost.setHeader("Accept", "application/x-thrift");
134    httpPost.setHeader("User-Agent", "Java/THttpClient/HC");
135  }
136
137  @After
138  public void tearDown() throws IOException {
139    if (httpPost != null) {
140      httpPost.releaseConnection();
141    }
142    if (tsr != null) {
143      tsr.close();
144    }
145  }
146
147  @Test
148  public void testSecurityHeaders() throws Exception {
149    try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
150      TMemoryBuffer memoryBuffer = new TMemoryBuffer(100);
151      TProtocol prot = new TBinaryProtocol(memoryBuffer);
152      Hbase.Client client = new Hbase.Client(prot);
153      client.send_getClusterId();
154
155      httpPost.setEntity(new ByteArrayEntity(memoryBuffer.getArray()));
156      CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
157
158      assertEquals(HttpURLConnection.HTTP_OK, httpResponse.getStatusLine().getStatusCode());
159      assertEquals("DENY", httpResponse.getFirstHeader("X-Frame-Options").getValue());
160
161      assertEquals("nosniff", httpResponse.getFirstHeader("X-Content-Type-Options").getValue());
162      assertEquals("1; mode=block", httpResponse.getFirstHeader("X-XSS-Protection").getValue());
163
164      assertEquals("default-src https: data: 'unsafe-inline' 'unsafe-eval'",
165        httpResponse.getFirstHeader("Content-Security-Policy").getValue());
166      assertEquals("max-age=63072000;includeSubDomains;preload",
167        httpResponse.getFirstHeader("Strict-Transport-Security").getValue());
168    }
169  }
170
171  // Workaround for jdk8 292 bug. See https://github.com/bcgit/bc-java/issues/941
172  // Below is a workaround described in above URL. Issue fingered first in comments in
173  // HBASE-25920 Support Hadoop 3.3.1
174  private static void initializeAlgorithmId() {
175    try {
176      Class<?> algoId = Class.forName("sun.security.x509.AlgorithmId");
177      Method method = algoId.getMethod("get", String.class);
178      method.setAccessible(true);
179      method.invoke(null, "PBEWithSHA1AndDESede");
180    } catch (Exception e) {
181      LOG.warn("failed to initialize AlgorithmId", e);
182    }
183  }
184
185  private File initKeystoreDir() {
186    String dataTestDir = TEST_UTIL.getDataTestDir().toString();
187    File keystoreDir = new File(dataTestDir, TestThriftHttpServer.class.getSimpleName() + "_keys");
188    keystoreDir.mkdirs();
189    return keystoreDir;
190  }
191
192  private void generateKeyStore(KeyPair keyPair, X509Certificate serverCertificate)
193    throws Exception {
194    String keyStorePath = getKeystoreFilePath();
195    KeyStoreTestUtil.createKeyStore(keyStorePath, KEY_STORE_PASSWORD, KEY_STORE_PASSWORD,
196      "serverKS", keyPair.getPrivate(), serverCertificate);
197  }
198
199  private void generateTrustStore(X509Certificate serverCertificate) throws Exception {
200    String trustStorePath = getTruststoreFilePath();
201    KeyStoreTestUtil.createTrustStore(trustStorePath, TRUST_STORE_PASSWORD, "serverTS",
202      serverCertificate);
203  }
204
205  private String getKeystoreFilePath() {
206    return String.format("%s/serverKS.%s", keyDir.getAbsolutePath(), "jks");
207  }
208
209  private String getTruststoreFilePath() {
210    return String.format("%s/serverTS.%s", keyDir.getAbsolutePath(), "jks");
211  }
212}