Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions core/src/main/java/org/apache/iceberg/rest/HTTPClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@
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;
Expand All @@ -50,6 +53,7 @@
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;
Expand All @@ -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";
Expand Down Expand Up @@ -443,6 +451,32 @@ public HTTPClient build() {
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");
Expand Down
59 changes: 59 additions & 0 deletions core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,65 @@ public void testProxyServerWithNullHostname() {
.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;
Expand Down