|
35 | 35 | import com.fasterxml.jackson.databind.ObjectMapper; |
36 | 36 | import java.io.IOException; |
37 | 37 | import java.net.SocketTimeoutException; |
| 38 | +import java.nio.file.Path; |
| 39 | +import java.security.KeyStore; |
| 40 | +import java.security.cert.CertificateException; |
38 | 41 | import java.util.Locale; |
39 | 42 | import java.util.Map; |
40 | 43 | import java.util.Objects; |
41 | 44 | import java.util.concurrent.TimeUnit; |
42 | 45 | import java.util.function.Consumer; |
| 46 | +import javax.net.ssl.HostnameVerifier; |
| 47 | +import javax.net.ssl.SSLContext; |
| 48 | +import javax.net.ssl.TrustManagerFactory; |
43 | 49 | import org.apache.hc.client5.http.auth.AuthScope; |
44 | 50 | import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; |
45 | 51 | import org.apache.hc.client5.http.config.ConnectionConfig; |
46 | 52 | import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; |
47 | 53 | import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; |
48 | 54 | import org.apache.hc.client5.http.io.HttpClientConnectionManager; |
| 55 | +import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy; |
| 56 | +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; |
49 | 57 | import org.apache.hc.core5.http.HttpHost; |
50 | 58 | import org.apache.hc.core5.http.HttpStatus; |
51 | 59 | import org.apache.iceberg.IcebergBuild; |
52 | 60 | import org.apache.iceberg.catalog.TableIdentifier; |
53 | 61 | import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; |
| 62 | +import org.apache.iceberg.relocated.com.google.common.collect.Sets; |
54 | 63 | import org.apache.iceberg.rest.auth.AuthSession; |
55 | 64 | import org.apache.iceberg.rest.auth.TLSConfigurer; |
56 | 65 | import org.apache.iceberg.rest.responses.ErrorResponse; |
57 | 66 | import org.apache.iceberg.rest.responses.ErrorResponseParser; |
58 | 67 | import org.junit.jupiter.api.AfterAll; |
59 | 68 | import org.junit.jupiter.api.BeforeAll; |
60 | 69 | import org.junit.jupiter.api.Test; |
| 70 | +import org.junit.jupiter.api.io.TempDir; |
61 | 71 | import org.junit.jupiter.params.ParameterizedTest; |
62 | 72 | import org.junit.jupiter.params.provider.EnumSource; |
63 | 73 | import org.junit.jupiter.params.provider.ValueSource; |
64 | 74 | import org.mockserver.configuration.Configuration; |
65 | 75 | import org.mockserver.integration.ClientAndServer; |
| 76 | +import org.mockserver.logging.MockServerLogger; |
66 | 77 | import org.mockserver.matchers.Times; |
67 | 78 | import org.mockserver.model.HttpRequest; |
68 | 79 | import org.mockserver.model.HttpResponse; |
| 80 | +import org.mockserver.socket.tls.KeyStoreFactory; |
69 | 81 | import org.mockserver.verify.VerificationTimes; |
70 | 82 |
|
71 | 83 | /** |
@@ -395,6 +407,99 @@ public void testLoadTLSConfigurerNotImplementTLSConfigurer() { |
395 | 407 | .hasMessageContaining("does not implement TLSConfigurer"); |
396 | 408 | } |
397 | 409 |
|
| 410 | + public static class LaxTLSConfigurer implements TLSConfigurer { |
| 411 | + |
| 412 | + private static HostnameVerificationPolicy policy = HostnameVerificationPolicy.BUILTIN; |
| 413 | + |
| 414 | + static void setPolicy(HostnameVerificationPolicy policy) { |
| 415 | + LaxTLSConfigurer.policy = policy; |
| 416 | + } |
| 417 | + |
| 418 | + @Override |
| 419 | + public SSLContext sslContext() { |
| 420 | + // Create an SSLContext that trusts MockServer's CA certificate but still performs hostname |
| 421 | + // verification during SSL handshake |
| 422 | + try { |
| 423 | + KeyStore keyStore = |
| 424 | + new KeyStoreFactory(Configuration.configuration(), new MockServerLogger()) |
| 425 | + .loadOrCreateKeyStore(); |
| 426 | + TrustManagerFactory tmf = |
| 427 | + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); |
| 428 | + tmf.init(keyStore); |
| 429 | + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); |
| 430 | + sslContext.init(null, tmf.getTrustManagers(), null); |
| 431 | + return sslContext; |
| 432 | + } catch (Exception e) { |
| 433 | + throw new RuntimeException("Failed to create SSLContext", e); |
| 434 | + } |
| 435 | + } |
| 436 | + |
| 437 | + @Override |
| 438 | + public HostnameVerificationPolicy hostnameVerificationPolicy() { |
| 439 | + return policy; |
| 440 | + } |
| 441 | + |
| 442 | + @Override |
| 443 | + public HostnameVerifier hostnameVerifier() { |
| 444 | + // Disable hostname verification at HttpClient level (post SSL handshake) |
| 445 | + return NoopHostnameVerifier.INSTANCE; |
| 446 | + } |
| 447 | + } |
| 448 | + |
| 449 | + @ParameterizedTest |
| 450 | + @EnumSource(HostnameVerificationPolicy.class) |
| 451 | + public void testTLSConfigurerHostnameVerificationPolicy(HostnameVerificationPolicy policy, @TempDir Path temp) |
| 452 | + throws IOException { |
| 453 | + |
| 454 | + // Start a dedicated MockServer with a certificate that does NOT include 127.0.0.1 or localhost |
| 455 | + // in its SANs. Hostname verification must be disabled for this test to pass. |
| 456 | + Configuration tlsConfig = Configuration.configuration(); |
| 457 | + tlsConfig.proactivelyInitialiseTLS(true); |
| 458 | + tlsConfig.preventCertificateDynamicUpdate(true); |
| 459 | + tlsConfig.sslCertificateDomainName("example.com"); |
| 460 | + tlsConfig.sslSubjectAlternativeNameIps(Sets.newHashSet("1.2.3.4")); |
| 461 | + tlsConfig.sslSubjectAlternativeNameDomains(Sets.newHashSet("example.com")); |
| 462 | + tlsConfig.directoryToSaveDynamicSSLCertificate(temp.toFile().getAbsolutePath()); |
| 463 | + |
| 464 | + int tlsPort = PORT + 1; |
| 465 | + try (ClientAndServer server = startClientAndServer(tlsConfig, tlsPort)) { |
| 466 | + |
| 467 | + String path = "tls/path"; |
| 468 | + HttpRequest mockRequest = |
| 469 | + request() |
| 470 | + .withPath("/" + path) |
| 471 | + .withMethod(HttpMethod.HEAD.name().toUpperCase(Locale.ROOT)); |
| 472 | + HttpResponse mockResponse = response().withStatusCode(200).withBody("TLS response"); |
| 473 | + server.when(mockRequest).respond(mockResponse); |
| 474 | + |
| 475 | + LaxTLSConfigurer.setPolicy(policy); |
| 476 | + |
| 477 | + Map<String, String> properties = |
| 478 | + Map.of(HTTPClient.REST_TLS_CONFIGURER, LaxTLSConfigurer.class.getName()); |
| 479 | + |
| 480 | + try (HTTPClient client = |
| 481 | + HTTPClient.builder(properties) |
| 482 | + .uri(String.format("https://127.0.0.1:%d", tlsPort)) |
| 483 | + .withAuthSession(AuthSession.EMPTY) |
| 484 | + .build()) { |
| 485 | + |
| 486 | + if (policy == HostnameVerificationPolicy.CLIENT) { |
| 487 | + // With hostname verification performed by the HttpClient *only*, |
| 488 | + // the request should succeed |
| 489 | + assertThatCode(() -> client.head(path, Map.of(), (unused) -> {})) |
| 490 | + .doesNotThrowAnyException(); |
| 491 | + } else { |
| 492 | + // With hostname verification performed by the JSSE provider, |
| 493 | + // the request should fail with a CertificateException |
| 494 | + assertThatThrownBy(() -> client.head(path, Map.of(), (unused) -> {})) |
| 495 | + .rootCause() |
| 496 | + .isInstanceOf(CertificateException.class) |
| 497 | + .hasMessage("No subject alternative names matching IP address 127.0.0.1 found"); |
| 498 | + } |
| 499 | + } |
| 500 | + } |
| 501 | + } |
| 502 | + |
398 | 503 | @Test |
399 | 504 | public void testSocketTimeout() throws IOException { |
400 | 505 | long socketTimeoutMs = 2000L; |
|
0 commit comments