This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new a77aef2fa3 [#12300] feat(client:client-java): TLS support at the
HTTPClient layer- #12300 (#12449)
a77aef2fa3 is described below
commit a77aef2fa3de5818623a4f56a7083da5f5bc02ab
Author: Octavio Herrera Contreras <[email protected]>
AuthorDate: Thu Aug 13 19:00:21 2026 -0700
[#12300] feat(client:client-java): TLS support at the HTTPClient layer-
#12300 (#12449)
[Subtask] M2: TLS support at the HTTPClient layer #12300
### What changes were proposed in this pull request?
- Added TLSConfigurer: Default TLS interface. One note to the reviewer:
the hostname verifier still uses an import
org.apache.hc.client5.http.ssl.HttpsSupport to provide a default
hostname verifier. SSLContext follows the requested "no types from
implementation-scoped dependencies in its public signatures or default
method bodies". I want to clarify if an hc5 import would meet or fail
this request.
- Added TLSConfigurers: contains all builder methods for easier ux.
-Modified HTTPClient: Added optional TLS configuration
- Added TestHTTPClientTLS.java: covers 6 requested test + 2 extra early
fail tests that further prove requested functionality
- Modified TestHttpsServerAuthentication to avoid redundant code in
TestHTTPClientTLS by shifting a few functions into TestTlsServerUtils
- Modified clients/client-java/build.gradle.kts: added test
implementation to include testArtifacts published in M1
Fix: #12300
### Does this PR introduce _any_ user-facing change?
Yes, addition of TLSConfigurer + TLSConfigurer as described in M0.
### How was this patch tested?
Ran:
./gradlew rat
Passed
./gradlew :server-common:test --tests
"org.apache.gravitino.server.web.TestHttpsServerAuthentication"
Passed
./gradlew :clients:client-java:test --tests
"org.apache.gravitino.client.TestHTTPClientTLS"
Passed
---
clients/client-java/build.gradle.kts | 1 +
.../org/apache/gravitino/client/HTTPClient.java | 81 +++++++-
.../org/apache/gravitino/client/TLSConfigurer.java | 68 +++++++
.../apache/gravitino/client/TLSConfigurers.java | 182 ++++++++++++++++++
.../apache/gravitino/client/TestHTTPClientTLS.java | 207 +++++++++++++++++++++
.../server/web/TestHttpsServerAuthentication.java | 39 +---
.../gravitino/server/web/TestTlsServerUtils.java | 51 ++++-
7 files changed, 585 insertions(+), 44 deletions(-)
diff --git a/clients/client-java/build.gradle.kts
b/clients/client-java/build.gradle.kts
index b3850d71fe..5ddb7b79ee 100644
--- a/clients/client-java/build.gradle.kts
+++ b/clients/client-java/build.gradle.kts
@@ -46,6 +46,7 @@ dependencies {
testImplementation(project(":integration-test-common", "testArtifacts"))
testImplementation(project(":server"))
testImplementation(project(":server-common"))
+ testImplementation(project(":server-common", "testArtifacts"))
testImplementation(libs.awaitility)
testImplementation(libs.bundles.jersey)
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/HTTPClient.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/HTTPClient.java
index 2862ac9e7c..b6a9c03785 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/HTTPClient.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/HTTPClient.java
@@ -35,6 +35,8 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Version;
import org.apache.gravitino.auth.AuthConstants;
@@ -52,6 +54,7 @@ import
org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import
org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
@@ -64,6 +67,7 @@ import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.net.URIBuilder;
+import org.apache.hc.core5.reactor.ssl.SSLBufferMode;
/**
* An HttpClient for usage with the REST catalog.
@@ -99,7 +103,8 @@ public class HTTPClient implements RESTClient {
private volatile HandlerStatus handlerStatus = HandlerStatus.Start;
/**
- * Constructs an instance of HTTPClient with the provided information.
+ * Constructs an instance of HTTPClient with the provided information, with
a default null TLS
+ * configurer.
*
* @param uri The base URI of the REST API.
* @param baseHeaders A map of base headers to be included in all HTTP
requests.
@@ -115,13 +120,36 @@ public class HTTPClient implements RESTClient {
AuthDataProvider authDataProvider,
Runnable beforeConnectHandler,
Map<String, String> properties) {
+ this(uri, baseHeaders, objectMapper, authDataProvider,
beforeConnectHandler, properties, null);
+ }
+
+ /**
+ * Constructs an instance of HTTPClient with the provided information.
+ *
+ * @param uri The base URI of the REST API.
+ * @param baseHeaders A map of base headers to be included in all HTTP
requests.
+ * @param objectMapper The ObjectMapper used for JSON serialization and
deserialization.
+ * @param authDataProvider The provider of authentication data.
+ * @param beforeConnectHandler The function to be executed before connecting
to the server.
+ * @param properties A map of properties (key-value pairs) used to configure
the HTTP client.
+ * @param tlsConfigurer The TLSConfigurer used to configure TLS settings for
the HTTP client.
+ */
+ private HTTPClient(
+ String uri,
+ Map<String, String> baseHeaders,
+ ObjectMapper objectMapper,
+ AuthDataProvider authDataProvider,
+ Runnable beforeConnectHandler,
+ Map<String, String> properties,
+ TLSConfigurer tlsConfigurer) {
this.uri = uri;
this.mapper = objectMapper;
GravitinoClientConfiguration clientConfiguration =
GravitinoClientConfiguration.buildFromProperties(properties);
HttpClientBuilder clientBuilder = HttpClients.custom();
-
clientBuilder.setConnectionManager(configureConnectionManager(clientConfiguration));
+ clientBuilder.setConnectionManager(
+ configureConnectionManager(clientConfiguration, tlsConfigurer));
if (baseHeaders != null) {
clientBuilder.setDefaultHeaders(
@@ -746,7 +774,7 @@ public class HTTPClient implements RESTClient {
}
private static HttpClientConnectionManager configureConnectionManager(
- GravitinoClientConfiguration clientConfiguration) {
+ GravitinoClientConfiguration clientConfiguration, TLSConfigurer
tlsConfigurer) {
PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder =
PoolingHttpClientConnectionManagerBuilder.create();
@@ -757,6 +785,26 @@ public class HTTPClient implements RESTClient {
ConnectionConfig connectionConfig =
configureConnectionConfig(clientConfiguration);
connectionManagerBuilder.setDefaultConnectionConfig(connectionConfig);
+
+ // Configure custom TLS settings, if provided
+ if (tlsConfigurer != null) {
+ SSLContext sslContext = requireTlsValue(tlsConfigurer.sslContext(),
"SSLContext");
+ HostnameVerifier hostnameVerifier =
+ requireTlsValue(tlsConfigurer.hostnameVerifier(),
"HostnameVerifier");
+ String[] protocols =
+ requireTlsValue(tlsConfigurer.supportedProtocols(), "supported
protocols");
+ String[] cipherSuites =
+ requireTlsValue(tlsConfigurer.supportedCipherSuites(), "supported
cipher suites");
+
+ connectionManagerBuilder.setTlsSocketStrategy(
+ new DefaultClientTlsStrategy(
+ sslContext,
+ protocols.length == 0 ? null : protocols,
+ cipherSuites.length == 0 ? null : cipherSuites,
+ SSLBufferMode.STATIC,
+ hostnameVerifier));
+ }
+
return connectionManagerBuilder.build();
}
@@ -782,6 +830,7 @@ public class HTTPClient implements RESTClient {
private final Map<String, String> properties;
private final Map<String, String> baseHeaders = Maps.newHashMap();
+ private TLSConfigurer tlsConfigurer;
private String uri;
private ObjectMapper mapper = ObjectMapperProvider.objectMapper();
private AuthDataProvider authDataProvider;
@@ -860,6 +909,17 @@ public class HTTPClient implements RESTClient {
return this;
}
+ /**
+ * Adds TLS configuration to the HTTP client.
+ *
+ * @param tlsConfigurer The TLS configurer for custom TLS settings.
+ * @return This Builder instance for method chaining.
+ */
+ public Builder withTlsConfigurer(TLSConfigurer tlsConfigurer) {
+ this.tlsConfigurer = tlsConfigurer;
+ return this;
+ }
+
/**
* Builds and returns an instance of the HTTPClient with the configured
options.
*
@@ -868,7 +928,13 @@ public class HTTPClient implements RESTClient {
public HTTPClient build() {
return new HTTPClient(
- uri, baseHeaders, mapper, authDataProvider, beforeConnectHandler,
properties);
+ uri,
+ baseHeaders,
+ mapper,
+ authDataProvider,
+ beforeConnectHandler,
+ properties,
+ tlsConfigurer);
}
}
@@ -883,4 +949,11 @@ public class HTTPClient implements RESTClient {
private StringEntity toFormEncoding(Map<?, ?> formData) {
return new StringEntity(RESTUtils.encodeFormData(formData));
}
+
+ private static <T> T requireTlsValue(T value, String name) {
+ if (value == null) {
+ throw new IllegalArgumentException("TLSConfigurer must provide a
non-null " + name + ".");
+ }
+ return value;
+ }
}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/TLSConfigurer.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/TLSConfigurer.java
new file mode 100644
index 0000000000..2a753aa317
--- /dev/null
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/TLSConfigurer.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.client;
+
+import java.security.NoSuchAlgorithmException;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import org.apache.hc.client5.http.ssl.HttpsSupport;
+
+/** Configures TLS settings for the HTTP client. */
+public interface TLSConfigurer {
+
+ /**
+ * Returns the SSL context used for TLS connections.
+ *
+ * @return SSL context
+ */
+ default SSLContext sslContext() {
+ try {
+ return SSLContext.getDefault();
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("Default SSLContext is unavailable", e);
+ }
+ }
+
+ /**
+ * Returns the hostname verifier used for TLS connections.
+ *
+ * @return hostname verifier
+ */
+ default HostnameVerifier hostnameVerifier() {
+ return HttpsSupport.getDefaultHostnameVerifier();
+ }
+
+ /**
+ * Returns the supported TLS protocols.
+ *
+ * @return supported TLS protocols
+ */
+ default String[] supportedProtocols() {
+ return new String[] {};
+ }
+
+ /**
+ * Returns the supported cipher suites.
+ *
+ * @return supported cipher suites
+ */
+ default String[] supportedCipherSuites() {
+ return new String[] {};
+ }
+}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/TLSConfigurers.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/TLSConfigurers.java
new file mode 100644
index 0000000000..c68512e233
--- /dev/null
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/TLSConfigurers.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.client;
+
+import com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+
+/** Provides utility methods for creating {@link TLSConfigurer} instances. */
+public final class TLSConfigurers {
+
+ private static final String DEFAULT_STORE_TYPE = "PKCS12";
+
+ private TLSConfigurers() {}
+
+ /**
+ * Returns a new builder for creating a {@link TLSConfigurer}.
+ *
+ * @return a new builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Builder for creating a {@link TLSConfigurer} from truststore and
keystore files. */
+ public static final class Builder {
+ private Path trustStorePath;
+ private String trustStorePassword;
+ private Path keyStorePath;
+ private String keyStorePassword;
+ private String storeType = DEFAULT_STORE_TYPE;
+
+ private Builder() {}
+
+ /**
+ * Configures the truststore used to verify the server certificate.
+ *
+ * @param path path to the truststore
+ * @param password password for the truststore
+ * @return this builder
+ */
+ public Builder trustStore(Path path, String password) {
+ this.trustStorePath = path;
+ this.trustStorePassword = password;
+ return this;
+ }
+
+ /**
+ * Configures the client keystore used for mutual TLS authentication.
+ *
+ * @param path path to the client keystore
+ * @param password password for the client keystore
+ * @return this builder
+ */
+ public Builder keyStore(Path path, String password) {
+ this.keyStorePath = path;
+ this.keyStorePassword = password;
+ return this;
+ }
+
+ /**
+ * Configures the store type used for both the truststore and client
keystore.
+ *
+ * <p>The default store type is {@code PKCS12}.
+ *
+ * @param storeType keystore and truststore type
+ * @return this builder
+ */
+ public Builder storeType(String storeType) {
+ this.storeType = storeType;
+ return this;
+ }
+
+ /**
+ * Builds a {@link TLSConfigurer} using the configured truststore and
optional client keystore.
+ *
+ * @return the configured TLS configurer
+ * @throws IllegalArgumentException if required configuration is missing
or the stores cannot be
+ * loaded
+ */
+ public TLSConfigurer build() {
+ Preconditions.checkArgument(trustStorePath != null, "Truststore path
must be provided");
+ Preconditions.checkArgument(
+ trustStorePassword != null, "Truststore password must be provided");
+ Preconditions.checkArgument(storeType != null, "Store type must be
provided");
+ Preconditions.checkArgument(
+ keyStorePath == null || keyStorePassword != null,
+ "Keystore password must be provided when a keystore is configured");
+
+ SSLContext sslContext = buildSslContext();
+
+ return new TLSConfigurer() {
+ @Override
+ public SSLContext sslContext() {
+ return sslContext;
+ }
+ };
+ }
+
+ /**
+ * Creates an SSL context from the configured truststore and optional
client keystore.
+ *
+ * @return the configured SSL context
+ * @throws IllegalArgumentException if the SSL context cannot be created
+ */
+ private SSLContext buildSslContext() {
+ try {
+ KeyStore trustStore = loadStore(trustStorePath, trustStorePassword,
"truststore");
+
+ TrustManagerFactory trustManagerFactory =
+
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(trustStore);
+
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+
+ if (keyStorePath == null) {
+ sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
+ return sslContext;
+ }
+
+ KeyStore keyStore = loadStore(keyStorePath, keyStorePassword,
"keystore");
+
+ KeyManagerFactory keyManagerFactory =
+
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
+
+ sslContext.init(
+ keyManagerFactory.getKeyManagers(),
trustManagerFactory.getTrustManagers(), null);
+
+ return sslContext;
+ } catch (GeneralSecurityException e) {
+ throw new IllegalArgumentException(
+ "Failed to configure TLS from the configured truststore or
keystore", e);
+ }
+ }
+
+ /**
+ * Loads a keystore from the provided path.
+ *
+ * @param path path to the keystore
+ * @param password password for the keystore
+ * @param storeName name of the store for error messages
+ * @return the loaded keystore
+ */
+ private KeyStore loadStore(Path path, String password, String storeName) {
+ try {
+ KeyStore keyStore = KeyStore.getInstance(storeType);
+
+ try (InputStream inputStream = Files.newInputStream(path)) {
+ keyStore.load(inputStream, password.toCharArray());
+ }
+
+ return keyStore;
+ } catch (IOException | GeneralSecurityException e) {
+ throw new IllegalArgumentException("Failed to load TLS " + storeName +
" from " + path, e);
+ }
+ }
+ }
+}
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestHTTPClientTLS.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestHTTPClientTLS.java
new file mode 100644
index 0000000000..b77752709d
--- /dev/null
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestHTTPClientTLS.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.client;
+
+import static
org.apache.gravitino.server.web.TestTlsServerUtils.TEST_STORE_PASSWORD;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.nio.file.Path;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.gravitino.exceptions.RESTException;
+import org.apache.gravitino.server.web.JettyServer;
+import org.apache.gravitino.server.web.TestTlsServerUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestHTTPClientTLS {
+ private static final String TEST_SERVLET_PATH = "/tls-test";
+ private static final String TEST_CLIENT_PATH = "tls-test";
+
+ private JettyServer jettyServer;
+
+ @BeforeEach
+ public void setUp() {
+ jettyServer = new JettyServer();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ if (jettyServer != null) {
+ jettyServer.stop();
+ }
+ }
+
+ @Test
+ public void testMutualTlsAcceptsTrustedClientCertificate() throws Exception {
+ int port =
+ TestTlsServerUtils.startHttpsServer(
+ jettyServer, true, createTestServlet(), TEST_SERVLET_PATH);
+
+ TLSConfigurer tlsConfigurer =
+ TLSConfigurers.builder()
+ .trustStore(
+ TestTlsServerUtils.testResource("test-client-truststore.p12"),
TEST_STORE_PASSWORD)
+ .keyStore(
+
TestTlsServerUtils.testResource("test-trusted-client-keystore.p12"),
+ TEST_STORE_PASSWORD)
+ .build();
+
+ try (HTTPClient client = createHttpsClient(port, tlsConfigurer)) {
+ client.head(TEST_CLIENT_PATH, ImmutableMap.of(), response -> {});
+ }
+ }
+
+ @Test
+ public void testMutualTlsRejectsMissingClientCertificate() throws Exception {
+ int port =
+ TestTlsServerUtils.startHttpsServer(
+ jettyServer, true, createTestServlet(), TEST_SERVLET_PATH);
+
+ TLSConfigurer tlsConfigurer =
+ TLSConfigurers.builder()
+ .trustStore(
+ TestTlsServerUtils.testResource("test-client-truststore.p12"),
TEST_STORE_PASSWORD)
+ .build();
+
+ try (HTTPClient client = createHttpsClient(port, tlsConfigurer)) {
+ RESTException exception =
+ assertThrows(
+ RESTException.class,
+ () -> client.head(TEST_CLIENT_PATH, ImmutableMap.of(), response
-> {}));
+
+ TestTlsServerUtils.assertHandshakeFailure(exception);
+ }
+ }
+
+ @Test
+ public void testMutualTlsRejectsUntrustedClientCertificate() throws
Exception {
+ int port =
+ TestTlsServerUtils.startHttpsServer(
+ jettyServer, true, createTestServlet(), TEST_SERVLET_PATH);
+
+ TLSConfigurer tlsConfigurer =
+ TLSConfigurers.builder()
+ .trustStore(
+ TestTlsServerUtils.testResource("test-client-truststore.p12"),
TEST_STORE_PASSWORD)
+ .keyStore(
+
TestTlsServerUtils.testResource("test-untrusted-client-keystore.p12"),
+ TEST_STORE_PASSWORD)
+ .build();
+
+ try (HTTPClient client = createHttpsClient(port, tlsConfigurer)) {
+ RESTException exception =
+ assertThrows(
+ RESTException.class,
+ () -> client.head(TEST_CLIENT_PATH, ImmutableMap.of(), response
-> {}));
+
+ TestTlsServerUtils.assertHandshakeFailure(exception);
+ }
+ }
+
+ @Test
+ public void testHttpsWithoutClientAuthentication() throws Exception {
+ int port =
+ TestTlsServerUtils.startHttpsServer(
+ jettyServer, false, createTestServlet(), TEST_SERVLET_PATH);
+
+ TLSConfigurer tlsConfigurer =
+ TLSConfigurers.builder()
+ .trustStore(
+ TestTlsServerUtils.testResource("test-client-truststore.p12"),
TEST_STORE_PASSWORD)
+ .build();
+
+ try (HTTPClient client = createHttpsClient(port, tlsConfigurer)) {
+ client.head(TEST_CLIENT_PATH, ImmutableMap.of(), response -> {});
+ }
+ }
+
+ @Test
+ public void testClientRejectsUntrustedServerCertificate() throws Exception {
+ int port =
+ TestTlsServerUtils.startHttpsServer(
+ jettyServer, false, createTestServlet(), TEST_SERVLET_PATH);
+
+ TLSConfigurer tlsConfigurer =
+ TLSConfigurers.builder()
+ .trustStore(
+
TestTlsServerUtils.testResource("test-untrusted-server-truststore.p12"),
+ TEST_STORE_PASSWORD)
+ .build();
+
+ try (HTTPClient client = createHttpsClient(port, tlsConfigurer)) {
+ RESTException exception =
+ assertThrows(
+ RESTException.class,
+ () -> client.head(TEST_CLIENT_PATH, ImmutableMap.of(), response
-> {}));
+
+ TestTlsServerUtils.assertHandshakeFailure(exception);
+ }
+ }
+
+ @Test
+ public void testHttpWithoutCustomTlsConfiguration() throws Exception {
+ int port =
+ TestTlsServerUtils.startHttpServer(jettyServer, createTestServlet(),
TEST_SERVLET_PATH);
+
+ try (HTTPClient client =
+ HTTPClient.builder(ImmutableMap.of()).uri("http://localhost:" +
port).build()) {
+ client.head(TEST_CLIENT_PATH, ImmutableMap.of(), response -> {});
+ }
+ }
+
+ @Test
+ public void testInvalidTrustStorePathFailsAtBuild() {
+ Path missingPath = Path.of("does-not-exist.p12");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> TLSConfigurers.builder().trustStore(missingPath,
TEST_STORE_PASSWORD).build());
+ }
+
+ @Test
+ public void testWrongTrustStorePasswordFailsAtBuild() throws Exception {
+ Path trustStore =
TestTlsServerUtils.testResource("test-client-truststore.p12");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> TLSConfigurers.builder().trustStore(trustStore,
"wrong-password").build());
+ }
+
+ private static HTTPClient createHttpsClient(int port, TLSConfigurer
tlsConfigurer) {
+ return HTTPClient.builder(ImmutableMap.of())
+ .uri("https://localhost:" + port)
+ .withTlsConfigurer(tlsConfigurer)
+ .build();
+ }
+
+ private static HttpServlet createTestServlet() {
+ return new HttpServlet() {
+ @Override
+ protected void doHead(HttpServletRequest request, HttpServletResponse
response)
+ throws IOException {
+ response.setStatus(HttpServletResponse.SC_OK);
+ }
+ };
+ }
+}
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/web/TestHttpsServerAuthentication.java
b/server-common/src/test/java/org/apache/gravitino/server/web/TestHttpsServerAuthentication.java
index 35cfbf1b82..df7c5d1516 100644
---
a/server-common/src/test/java/org/apache/gravitino/server/web/TestHttpsServerAuthentication.java
+++
b/server-common/src/test/java/org/apache/gravitino/server/web/TestHttpsServerAuthentication.java
@@ -22,7 +22,6 @@ package org.apache.gravitino.server.web;
import static
org.apache.gravitino.server.web.TestTlsServerUtils.TEST_STORE_PASSWORD;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.fail;
import java.io.IOException;
import java.io.InputStream;
@@ -36,7 +35,6 @@ import java.security.KeyStore;
import java.time.Duration;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManagerFactory;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
@@ -119,7 +117,7 @@ public class TestHttpsServerAuthentication {
assertThrows(
Exception.class, () -> client.send(request,
HttpResponse.BodyHandlers.ofString()));
- assertHandshakeFailure(exception);
+ TestTlsServerUtils.assertHandshakeFailure(exception);
}
@Test
@@ -138,7 +136,7 @@ public class TestHttpsServerAuthentication {
assertThrows(
Exception.class, () -> client.send(request,
HttpResponse.BodyHandlers.ofString()));
- assertHandshakeFailure(exception);
+ TestTlsServerUtils.assertHandshakeFailure(exception);
}
@Test
@@ -156,7 +154,7 @@ public class TestHttpsServerAuthentication {
assertThrows(
Exception.class, () -> client.send(request,
HttpResponse.BodyHandlers.ofString()));
- assertHandshakeFailure(exception);
+ TestTlsServerUtils.assertHandshakeFailure(exception);
}
@Test
@@ -177,7 +175,7 @@ public class TestHttpsServerAuthentication {
@Test
public void testHttpWithoutCustomTlsConfiguration() throws Exception {
- int port = startHttpServer(jettyServer, createTestServlet(), TEST_PATH);
+ int port = TestTlsServerUtils.startHttpServer(jettyServer,
createTestServlet(), TEST_PATH);
HttpRequest request =
HttpRequest.newBuilder()
@@ -193,21 +191,6 @@ public class TestHttpsServerAuthentication {
assertEquals(TEST_RESPONSE, response.body());
}
- public static int startHttpServer(JettyServer server, HttpServlet servlet,
String servletPath)
- throws Exception {
-
- int port = RESTUtils.findAvailablePort(5000, 6000);
-
- Config config = new Config(false) {};
- config.set(JettyServerConfig.WEBSERVER_HTTP_PORT, port);
-
- server.initialize(JettyServerConfig.fromConfig(config), "test", false);
- server.start();
- server.addServlet(servlet, servletPath);
-
- return port;
- }
-
private static HttpClient createHttpsClient(Path trustStorePath, Path
clientKeyStorePath)
throws Exception {
KeyStore trustStore = loadStore(trustStorePath);
@@ -266,18 +249,4 @@ public class TestHttpsServerAuthentication {
}
};
}
-
- private static void assertHandshakeFailure(Throwable throwable) {
- Throwable current = throwable;
-
- while (current != null) {
- if (current instanceof SSLHandshakeException) {
- return;
- }
-
- current = current.getCause();
- }
-
- fail("Expected an SSL handshake failure, but received: " + throwable,
throwable);
- }
}
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/web/TestTlsServerUtils.java
b/server-common/src/test/java/org/apache/gravitino/server/web/TestTlsServerUtils.java
index 83e3b48b50..7682442a1e 100644
---
a/server-common/src/test/java/org/apache/gravitino/server/web/TestTlsServerUtils.java
+++
b/server-common/src/test/java/org/apache/gravitino/server/web/TestTlsServerUtils.java
@@ -19,8 +19,14 @@
package org.apache.gravitino.server.web;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.InputStream;
+import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.util.Objects;
+import javax.net.ssl.SSLHandshakeException;
import javax.servlet.http.HttpServlet;
import org.apache.gravitino.Config;
import org.apache.gravitino.rest.RESTUtils;
@@ -62,11 +68,46 @@ public final class TestTlsServerUtils {
return port;
}
+ public static int startHttpServer(JettyServer server, HttpServlet servlet,
String servletPath)
+ throws Exception {
+
+ int port = RESTUtils.findAvailablePort(6000, 7000);
+
+ Config config = new Config(false) {};
+ config.set(JettyServerConfig.WEBSERVER_HTTP_PORT, port);
+
+ server.initialize(JettyServerConfig.fromConfig(config), "test", false);
+ server.start();
+ server.addServlet(servlet, servletPath);
+
+ return port;
+ }
+
public static Path testResource(String filename) throws Exception {
- return Path.of(
- Objects.requireNonNull(
- TestTlsServerUtils.class.getResource("/tls/" + filename),
- "Missing TLS test resource: " + filename)
- .toURI());
+ try (InputStream inputStream =
+ TestTlsServerUtils.class.getResourceAsStream("/tls/" + filename)) {
+
+ Objects.requireNonNull(inputStream, "Missing TLS test resource: " +
filename);
+
+ Path tempFile = Files.createTempFile("gravitino-tls-", "-" + filename);
+ Files.copy(inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING);
+ tempFile.toFile().deleteOnExit();
+
+ return tempFile;
+ }
+ }
+
+ public static void assertHandshakeFailure(Throwable throwable) {
+ Throwable current = throwable;
+
+ while (current != null) {
+ if (current instanceof SSLHandshakeException) {
+ return;
+ }
+
+ current = current.getCause();
+ }
+
+ fail("Expected an SSL handshake failure, but received: " + throwable,
throwable);
}
}