This is an automated email from the ASF dual-hosted git repository.

yufei pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new a04c532650 Core: Enable HTTP proxy support for the client used by REST 
Catalog (#12406)
a04c532650 is described below

commit a04c532650156c0e3cb0de56cee48ddfb8e8fd2a
Author: Akhil Lawrence <[email protected]>
AuthorDate: Wed May 7 04:23:57 2025 +0530

    Core: Enable HTTP proxy support for the client used by REST Catalog (#12406)
---
 .../java/org/apache/iceberg/rest/HTTPClient.java   | 34 +++++++++++++
 .../org/apache/iceberg/rest/TestHTTPClient.java    | 59 ++++++++++++++++++++++
 2 files changed, 93 insertions(+)

diff --git a/core/src/main/java/org/apache/iceberg/rest/HTTPClient.java 
b/core/src/main/java/org/apache/iceberg/rest/HTTPClient.java
index 2f465e0450..532fff2401 100644
--- a/core/src/main/java/org/apache/iceberg/rest/HTTPClient.java
+++ b/core/src/main/java/org/apache/iceberg/rest/HTTPClient.java
@@ -27,9 +27,12 @@ import java.nio.charset.StandardCharsets;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
+import org.apache.hc.client5.http.auth.AuthScope;
 import org.apache.hc.client5.http.auth.CredentialsProvider;
+import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
 import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
 import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
 import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
@@ -50,6 +53,7 @@ import org.apache.iceberg.IcebergBuild;
 import org.apache.iceberg.exceptions.RESTException;
 import 
org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
 import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.base.Strings;
 import org.apache.iceberg.relocated.com.google.common.collect.Maps;
 import org.apache.iceberg.rest.HTTPRequest.HTTPMethod;
 import org.apache.iceberg.rest.auth.AuthSession;
@@ -72,6 +76,10 @@ public class HTTPClient extends BaseHTTPClient {
   static final int REST_MAX_CONNECTIONS_DEFAULT = 100;
   static final String REST_MAX_CONNECTIONS_PER_ROUTE = 
"rest.client.connections-per-route";
   static final int REST_MAX_CONNECTIONS_PER_ROUTE_DEFAULT = 100;
+  static final String REST_PROXY_HOSTNAME = "rest.client.proxy.hostname";
+  static final String REST_PROXY_PORT = "rest.client.proxy.port";
+  static final String REST_PROXY_USERNAME = "rest.client.proxy.username";
+  static final String REST_PROXY_PASSWORD = "rest.client.proxy.password";
 
   @VisibleForTesting
   static final String REST_CONNECTION_TIMEOUT_MS = 
"rest.client.connection-timeout-ms";
@@ -443,6 +451,32 @@ public class HTTPClient extends BaseHTTPClient {
       withHeader(CLIENT_VERSION_HEADER, IcebergBuild.fullVersion());
       withHeader(CLIENT_GIT_COMMIT_SHORT_HEADER, 
IcebergBuild.gitCommitShortId());
 
+      String proxyHostname =
+          PropertyUtil.propertyAsString(properties, 
HTTPClient.REST_PROXY_HOSTNAME, null);
+
+      Integer proxyPort =
+          PropertyUtil.propertyAsNullableInt(properties, 
HTTPClient.REST_PROXY_PORT);
+
+      if (!Strings.isNullOrEmpty(proxyHostname) && proxyPort != null) {
+        withProxy(proxyHostname, proxyPort);
+
+        String proxyUsername =
+            PropertyUtil.propertyAsString(properties, 
HTTPClient.REST_PROXY_USERNAME, null);
+
+        String proxyPassword =
+            PropertyUtil.propertyAsString(properties, 
HTTPClient.REST_PROXY_PASSWORD, null);
+
+        if (!Strings.isNullOrEmpty(proxyUsername) && 
!Strings.isNullOrEmpty(proxyPassword)) {
+          // currently only basic auth is supported
+          BasicCredentialsProvider credentialProvider = new 
BasicCredentialsProvider();
+          credentialProvider.setCredentials(
+              new AuthScope(proxyHostname, proxyPort),
+              new UsernamePasswordCredentials(proxyUsername, 
proxyPassword.toCharArray()));
+
+          withProxyCredentialsProvider(credentialProvider);
+        }
+      }
+
       if (this.proxyCredentialsProvider != null) {
         Preconditions.checkNotNull(
             proxy, "Invalid http client proxy for proxy credentials provider: 
null");
diff --git a/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java 
b/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java
index 760ed840de..33984307b8 100644
--- a/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java
+++ b/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java
@@ -174,6 +174,65 @@ public class TestHTTPClient {
         .hasMessage("Invalid hostname for http client proxy: null");
   }
 
+  @Test
+  public void testClientWithProxyProps() throws IOException {
+    int proxyPort = 1070;
+    try (ClientAndServer proxyServer = startClientAndServer(proxyPort);
+        RESTClient clientWithProxy =
+            HTTPClient.builder(
+                    ImmutableMap.of(
+                        HTTPClient.REST_PROXY_HOSTNAME,
+                        "localhost",
+                        HTTPClient.REST_PROXY_PORT,
+                        String.valueOf(proxyPort)))
+                .uri(URI)
+                .withAuthSession(AuthSession.EMPTY)
+                .build()) {
+      String path = "v1/config";
+      HttpRequest mockRequest =
+          request("/" + 
path).withMethod(HttpMethod.HEAD.name().toUpperCase(Locale.ROOT));
+      HttpResponse mockResponse = response().withStatusCode(200);
+      proxyServer.when(mockRequest).respond(mockResponse);
+      clientWithProxy.head(path, ImmutableMap.of(), (onError) -> {});
+      proxyServer.verify(mockRequest, VerificationTimes.exactly(1));
+    }
+  }
+
+  @Test
+  public void testClientWithAuthProxyProps() throws IOException {
+    int proxyPort = 1070;
+    String authorizedUsername = "test-username";
+    String authorizedPassword = "test-password";
+    try (ClientAndServer proxyServer =
+            startClientAndServer(
+                new Configuration()
+                    .proxyAuthenticationUsername(authorizedUsername)
+                    .proxyAuthenticationPassword(authorizedPassword),
+                proxyPort);
+        RESTClient clientWithProxy =
+            HTTPClient.builder(
+                    ImmutableMap.of(
+                        HTTPClient.REST_PROXY_HOSTNAME,
+                        "localhost",
+                        HTTPClient.REST_PROXY_PORT,
+                        String.valueOf(proxyPort),
+                        HTTPClient.REST_PROXY_USERNAME,
+                        authorizedUsername,
+                        HTTPClient.REST_PROXY_PASSWORD,
+                        authorizedPassword))
+                .uri(URI)
+                .withAuthSession(AuthSession.EMPTY)
+                .build()) {
+      String path = "v1/config";
+      HttpRequest mockRequest =
+          request("/" + 
path).withMethod(HttpMethod.HEAD.name().toUpperCase(Locale.ROOT));
+      HttpResponse mockResponse = response().withStatusCode(200);
+      proxyServer.when(mockRequest).respond(mockResponse);
+      clientWithProxy.head(path, ImmutableMap.of(), (onError) -> {});
+      proxyServer.verify(mockRequest, VerificationTimes.exactly(1));
+    }
+  }
+
   @Test
   public void testProxyAuthenticationFailure() throws IOException {
     int proxyPort = 1050;

Reply via email to