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