Skip to content

Commit

Permalink
Add support for routing through an HTTPS proxy (#2279)
Browse files Browse the repository at this point in the history
<!-- User-facing outcomes this PR delivers -->
  • Loading branch information
Eric-Alvarez authored May 13, 2024
1 parent ea604e7 commit 510e16b
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 26 deletions.
6 changes: 6 additions & 0 deletions changelog/@unreleased/pr-2279.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type: feature
feature:
description: Add support for routing through HTTPS proxies in the apache client
when configured
links:
- https://github.com/palantir/dialogue/pull/2279
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import java.net.ProxySelector;
import javax.annotation.Nullable;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
import org.apache.hc.client5.http.routing.HttpRoutePlanner;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
Expand All @@ -37,7 +36,7 @@ final class DialogueRoutePlanner implements HttpRoutePlanner {
private final HttpRoutePlanner delegate;

DialogueRoutePlanner(ProxySelector proxySelector) {
delegate = new SystemDefaultRoutePlanner(proxySelector);
delegate = new HttpsProxyDefaultRoutePlanner(proxySelector);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*
* Licensed 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 com.palantir.dialogue.hc5;

import com.palantir.conjure.java.client.config.HttpsProxies;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import javax.annotation.CheckForNull;
import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner;
import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.protocol.HttpContext;

/**
* Identical to {@link SystemDefaultRoutePlanner} but adds support for connecting to an HTTPS proxy.
* Original version is licensed under Apache License, Version 2.0. The original source code can be found
* <a href="https://github.com/apache/httpcomponents-client/blob/rel/v5.3.1/httpclient5/src/main/java/org/apache/hc/client5/http/impl/routing/SystemDefaultRoutePlanner.java">here</a>.
* <p>
* For future changes and updates in the {@link SystemDefaultRoutePlanner} implementation, refer to the linked version
* that this implementation was based off of provided to determine if this class requires changes.
*/
final class HttpsProxyDefaultRoutePlanner extends DefaultRoutePlanner {
private final ProxySelector proxySelector;

HttpsProxyDefaultRoutePlanner(ProxySelector proxySelector) {
super(null);
this.proxySelector = proxySelector;
}

@Override
@CheckForNull
public HttpHost determineProxy(final HttpHost target, final HttpContext _context) throws HttpException {
final URI targetUri;
try {
targetUri = new URI(target.toURI());
} catch (final URISyntaxException ex) {
throw new HttpException("Cannot convert host to URI: " + target, ex);
}
ProxySelector proxySelectorInstance = this.proxySelector;
if (proxySelectorInstance == null) {
proxySelectorInstance = ProxySelector.getDefault();
}
if (proxySelectorInstance == null) {
// The proxy selector can be "unset", so we must be able to deal with a null selector
return null;
}
final List<Proxy> proxies = proxySelectorInstance.select(targetUri);
final Proxy p = chooseProxy(proxies);
HttpHost result = null;
if (p.type() == Proxy.Type.HTTP) {
// convert the socket address to an HttpHost
if (!(p.address() instanceof InetSocketAddress)) {
throw new HttpException("Unable to handle non-Inet proxy address: " + p.address());
}
final InetSocketAddress isa = (InetSocketAddress) p.address();
String scheme = HttpsProxies.isHttps(p) ? "https" : "http";
result = new HttpHost(scheme, isa.getAddress(), isa.getHostString(), isa.getPort());
}

return result;
}

private Proxy chooseProxy(final List<Proxy> proxies) {
Proxy result = null;
// check the list for one we can use
for (int i = 0; (result == null) && (i < proxies.size()); i++) {
final Proxy p = proxies.get(i);
switch (p.type()) {
case DIRECT:
case HTTP:
result = p;
break;

case SOCKS:
// SOCKS hosts are not handled on the route level.
// The socket may make use of the SOCKS host though.
break;
}
}
if (result == null) {
// @@@ log as warning or info that only a socks proxy is available?
// result can only be null if all proxies are socks proxies
// socks proxies are not handled on the route planning level
result = Proxy.NO_PROXY;
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.google.common.util.concurrent.Uninterruptibles;
import com.palantir.conjure.java.api.config.service.BasicCredentials;
import com.palantir.conjure.java.client.config.ClientConfiguration;
import com.palantir.conjure.java.client.config.HttpsProxies;
import com.palantir.conjure.java.config.ssl.SslSocketFactories;
import io.undertow.Undertow;
import io.undertow.server.HttpHandler;
Expand All @@ -48,6 +49,9 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
* Parameterized tests are incompatible with BeforeEach, so the tests must be duplicated to test the two proxy types.
*/
public abstract class AbstractProxyConfigTlsTest {

private static final String REQUEST_BODY = "Hello, World";
Expand Down Expand Up @@ -79,6 +83,7 @@ public void close() {}
private volatile HttpHandler proxyHandler;

private int proxyPort;
private int httpsProxyPort;
private Undertow proxyServer;

protected abstract Channel create(ClientConfiguration config);
Expand All @@ -95,9 +100,21 @@ public void beforeEach() {
proxyServer = Undertow.builder()
.setHandler(exchange -> proxyHandler.handleRequest(exchange))
.addHttpListener(0, null)
.addHttpsListener(0, null, sslContext)
.build();
proxyServer.start();
proxyPort = getPort(proxyServer);
proxyPort = getProxyPort("http", "No HTTP listener");
httpsProxyPort = getProxyPort("https", "No HTTPS listener");
}

private Integer getProxyPort(String scheme, String errorMessage) {
return proxyServer.getListenerInfo().stream()
.filter(info -> info.getProtcol().equals(scheme))
.map(Undertow.ListenerInfo::getAddress)
.map(InetSocketAddress.class::cast)
.map(InetSocketAddress::getPort)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(errorMessage));
}

private static int getPort(Undertow undertow) {
Expand All @@ -116,7 +133,16 @@ public void afterEach() {
}

@Test
public void testDirectVersusProxyReadTimeout() throws Exception {
public void testDirectVersusProxyReadTimeout_httpProxy() throws Exception {
testDirectVersusProxyReadTimeout(createProxySelector("localhost", proxyPort, false));
}

@Test
public void testDirectProxyReadTimeout_httpsProxy() throws Exception {
testDirectVersusProxyReadTimeout(createProxySelector("localhost", httpsProxyPort, true));
}

private void testDirectVersusProxyReadTimeout(ProxySelector proxySelector) throws Exception {
serverHandler = new BlockingHandler(exchange -> {
Uninterruptibles.sleepUninterruptibly(Duration.ofSeconds(2));
exchange.getResponseSender().send("server");
Expand All @@ -132,7 +158,7 @@ public void testDirectVersusProxyReadTimeout() throws Exception {
Channel directChannel = create(directConfig);
ClientConfiguration proxiedConfig = ClientConfiguration.builder()
.from(directConfig)
.proxy(createProxySelector("localhost", proxyPort))
.proxy(proxySelector)
.build();
Channel proxiedChannel = create(proxiedConfig);

Expand All @@ -149,7 +175,16 @@ public void testDirectVersusProxyReadTimeout() throws Exception {
}

@Test
public void testAuthenticatedProxy() throws Exception {
public void testAuthenticatedProxy_httpProxy() throws Exception {
testAuthenticatedProxy(createProxySelector("localhost", proxyPort, false));
}

@Test
public void testAuthenticatedProxy_httpsProxy() throws Exception {
testAuthenticatedProxy(createProxySelector("localhost", httpsProxyPort, true));
}

private void testAuthenticatedProxy(ProxySelector proxySelector) throws Exception {
AtomicInteger requestIndex = new AtomicInteger();
proxyHandler = exchange -> {
HeaderMap requestHeaders = exchange.getRequestHeaders();
Expand Down Expand Up @@ -180,7 +215,7 @@ public void testAuthenticatedProxy() throws Exception {
ClientConfiguration proxiedConfig = ClientConfiguration.builder()
.from(TestConfigurations.create("https://localhost:" + serverPort))
.maxNumRetries(0)
.proxy(createProxySelector("localhost", proxyPort))
.proxy(proxySelector)
.proxyCredentials(BasicCredentials.of("[email protected]", "fake:Password"))
.build();
Channel proxiedChannel = create(proxiedConfig);
Expand All @@ -192,12 +227,12 @@ public void testAuthenticatedProxy() throws Exception {
}
}

private static ProxySelector createProxySelector(String host, int port) {
private static ProxySelector createProxySelector(String host, int port, boolean httpsProxy) {
return new ProxySelector() {
@Override
public List<Proxy> select(URI _uri) {
InetSocketAddress addr = InetSocketAddress.createUnresolved(host, port);
return ImmutableList.of(new Proxy(Proxy.Type.HTTP, addr));
return ImmutableList.of(httpsProxy ? HttpsProxies.create(addr) : new Proxy(Proxy.Type.HTTP, addr));
}

@Override
Expand Down
32 changes: 16 additions & 16 deletions versions.lock
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,36 @@ com.google.auto:auto-common:1.2.1 (1 constraints: 17120ffb)
com.google.code.findbugs:jsr305:3.0.2 (14 constraints: 49e26143)
com.google.errorprone:error_prone_annotations:2.7.1 (17 constraints: fe0f93dc)
com.google.guava:failureaccess:1.0.2 (1 constraints: 150ae2b4)
com.google.guava:guava:33.1.0-jre (16 constraints: c7183305)
com.google.guava:guava:33.2.0-jre (16 constraints: ca186609)
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava (1 constraints: bd17c918)
com.google.j2objc:j2objc-annotations:3.0.0 (1 constraints: 150aeab4)
com.palantir.common:streams:2.3.0 (1 constraints: 0705fe35)
com.palantir.conjure.java:conjure-lib:8.22.0 (1 constraints: 3e055d3b)
com.palantir.conjure.java.api:errors:2.52.0 (2 constraints: 5b159ada)
com.palantir.conjure.java.api:service-config:2.52.0 (2 constraints: 541918dc)
com.palantir.conjure.java.api:ssl-config:2.52.0 (4 constraints: f63e129b)
com.palantir.conjure.java.runtime:client-config:8.7.0 (1 constraints: 11052836)
com.palantir.conjure.java.runtime:conjure-java-jackson-optimizations:8.7.0 (1 constraints: 551c4588)
com.palantir.conjure.java.runtime:conjure-java-jackson-serialization:8.7.0 (2 constraints: 37160901)
com.palantir.conjure.java.runtime:keystores:8.7.0 (2 constraints: 011997b4)
com.palantir.conjure.java.api:errors:2.53.0 (2 constraints: 5b159ada)
com.palantir.conjure.java.api:service-config:2.53.0 (2 constraints: 56191edc)
com.palantir.conjure.java.api:ssl-config:2.53.0 (4 constraints: fb3ef99b)
com.palantir.conjure.java.runtime:client-config:8.9.0 (1 constraints: 13052e36)
com.palantir.conjure.java.runtime:conjure-java-jackson-optimizations:8.9.0 (1 constraints: 571c4b88)
com.palantir.conjure.java.runtime:conjure-java-jackson-serialization:8.9.0 (2 constraints: 39167101)
com.palantir.conjure.java.runtime:keystores:8.9.0 (2 constraints: 051913b5)
com.palantir.goethe:goethe:0.12.0 (1 constraints: 3505293b)
com.palantir.nylon:nylon-threads:0.4.0 (1 constraints: 0c10fa91)
com.palantir.refreshable:refreshable:2.3.0 (2 constraints: ed1813b2)
com.palantir.refreshable:refreshable:2.5.0 (2 constraints: ef1819b2)
com.palantir.ri:resource-identifier:2.7.0 (2 constraints: fb1464b7)
com.palantir.safe-logging:logger:3.7.0 (12 constraints: 4bc0c6d3)
com.palantir.safe-logging:logger:3.7.0 (12 constraints: 4cc096d4)
com.palantir.safe-logging:logger-slf4j:3.7.0 (1 constraints: 050e6842)
com.palantir.safe-logging:logger-spi:3.7.0 (2 constraints: 191ea27b)
com.palantir.safe-logging:preconditions:3.7.0 (17 constraints: 9c15bec6)
com.palantir.safe-logging:preconditions:3.7.0 (17 constraints: 9d15f4c7)
com.palantir.safe-logging:safe-logging:3.7.0 (17 constraints: 8d167d39)
com.palantir.safethreadlocalrandom:safe-thread-local-random:0.1.0 (1 constraints: 0305ee35)
com.palantir.tokens:auth-tokens:3.18.0 (3 constraints: 2628868e)
com.palantir.tracing:tracing:6.18.0 (2 constraints: 62165a0f)
com.palantir.tracing:tracing-api:6.18.0 (2 constraints: 17121d19)
com.palantir.tritium:tritium-api:0.87.0 (2 constraints: 3f1f46be)
com.palantir.tritium:tritium-core:0.87.0 (1 constraints: 47105da2)
com.palantir.tritium:tritium-ids:0.87.0 (1 constraints: d20fb596)
com.palantir.tritium:tritium-metrics:0.87.0 (1 constraints: 4105543b)
com.palantir.tritium:tritium-registry:0.87.0 (4 constraints: 2b46f68f)
com.palantir.tritium:tritium-api:0.88.0 (2 constraints: 411f7abe)
com.palantir.tritium:tritium-core:0.88.0 (1 constraints: 481060a2)
com.palantir.tritium:tritium-ids:0.88.0 (1 constraints: d30fb896)
com.palantir.tritium:tritium-metrics:0.88.0 (1 constraints: 4105543b)
com.palantir.tritium:tritium-registry:0.88.0 (4 constraints: 2e46a990)
com.squareup:javapoet:1.13.0 (2 constraints: 2b113eee)
io.dropwizard.metrics:metrics-core:4.2.25 (4 constraints: cb42cc11)
javax.annotation:javax.annotation-api:1.3.2 (1 constraints: 0805fb35)
Expand Down
2 changes: 1 addition & 1 deletion versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ com.palantir.common:streams = 2.3.0
com.palantir.conjure:conjure = 4.48.0
com.palantir.conjure.java:* = 8.22.0
com.palantir.conjure.java.api:* = 2.52.0
com.palantir.conjure.java.runtime:* = 8.7.0
com.palantir.conjure.java.runtime:* = 8.9.0
com.palantir.refreshable:* = 2.3.0
com.palantir.ri:resource-identifier = 2.7.0
com.palantir.safe-logging:* = 3.7.0
Expand Down

0 comments on commit 510e16b

Please sign in to comment.