Skip to content

Commit

Permalink
feat(rest): Improve http proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
ztefanie committed Feb 19, 2025
1 parent 4ae6c4e commit 9f9144c
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 241 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,12 @@
import io.camunda.connector.http.base.model.HttpCommonRequest;
import io.camunda.connector.http.base.model.HttpCommonResult;
import java.io.IOException;
import java.net.URISyntaxException;
import javax.annotation.Nullable;
import org.apache.hc.client5.http.ClientProtocolException;
import org.apache.hc.client5.http.config.RequestConfig;
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.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.util.Timeout;
Expand Down Expand Up @@ -103,15 +101,9 @@ public HttpCommonResult execute(
@Nullable ExecutionEnvironment executionEnvironment) {
try {
var apacheRequest = ApacheRequestFactory.get().createHttpRequest(request);
HttpHost proxy =
proxyHandler.getProxyHost(
apacheRequest.getUri().getScheme(), apacheRequest.getUri().getHost());
var routePlanner = proxyHandler.getRoutePlanner(apacheRequest.getUri().getScheme(), proxy);
var result =
httpClientBuilder
.setDefaultRequestConfig(getRequestConfig(request))
.setRoutePlanner(routePlanner)
.setProxy(proxy)
.setDefaultCredentialsProvider(
proxyHandler.getCredentialsProvider(apacheRequest.getScheme()))
.useSystemProperties() // Will fallback on system properties for unset values,
Expand All @@ -132,7 +124,7 @@ public HttpCommonResult execute(
String.valueOf(HttpStatus.SC_SERVER_ERROR),
"An error with the HTTP protocol occurred",
e);
} catch (IOException | URISyntaxException e) {
} catch (IOException e) {
throw new ConnectorException(
String.valueOf(HttpStatus.SC_REQUEST_TIMEOUT),
"An error occurred while executing the request, or the connection was aborted",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,16 @@

import io.camunda.connector.api.error.ConnectorInputException;
import java.net.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
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.impl.DefaultSchemePortResolver;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner;
import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
import org.apache.hc.client5.http.routing.HttpRoutePlanner;
import org.apache.hc.core5.http.HttpHost;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -56,6 +49,7 @@ record ProxyDetails(
public ProxyHandler() {
this.proxyConfigForProtocols = loadProxyConfig();
this.credentialsProvidersForProtocols = initializeCredentialsProviders();
this.syncEnvVarsToSystemProperties();
}

private Map<String, ProxyDetails> loadProxyConfig() {
Expand Down Expand Up @@ -99,7 +93,7 @@ private Optional<ProxyDetails> getConfigFromEnvVars(String protocol) {
System.getenv()
.getOrDefault("CONNECTOR_" + protocol.toUpperCase() + "_PROXY_PASSWORD", "")
.toCharArray(),
System.getenv("CONNECTOR_" + protocol.toUpperCase() + "_PROXY_NON_PROXY_HOSTS"),
System.getenv("CONNECTOR_HTTP_PROXY_NON_PROXY_HOSTS"),
false));
} catch (NumberFormatException e) {
throw new ConnectorInputException("Invalid proxy port in environment variables", e);
Expand Down Expand Up @@ -135,56 +129,31 @@ public CredentialsProvider getCredentialsProvider(String protocol) {
return credentialsProvidersForProtocols.getOrDefault(protocol, new BasicCredentialsProvider());
}

public HttpHost getProxyHost(String protocol, String requestUrl) {
ProxyDetails p = proxyConfigForProtocols.get(protocol);
if (p == null || doesTargetMatchNonProxy(protocol, requestUrl)) {
LOGGER.debug("No proxy used for request URL: {}", requestUrl);
return null;
}
LOGGER.debug(
"Using proxy for {} - Host: {}, Port: {}, Source: {}",
protocol,
p.host(),
p.port(),
p.sourceIsSystemProperties() ? "System Properties" : "Environment Variables");

return new HttpHost(p.protocol(), p.host(), p.port());
}

private boolean doesTargetMatchNonProxy(String protocol, String requestUri) {
ProxyDetails p = proxyConfigForProtocols.get(protocol);
if (p == null || p.nonProxyHosts() == null) {
return false;
/*
Set the system properties for the proxy settings from env vars, if needed, because
the default proxy selector does enforce a set of System Properties related to proxy settings.
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/net/ProxySelector.html
*/
private void syncEnvVarsToSystemProperties() {
for (Map.Entry<String, ProxyDetails> entry : proxyConfigForProtocols.entrySet()) {
ProxyDetails p = entry.getValue();
if (!p.sourceIsSystemProperties()) {
setSystemPropertyIfUnset(p.protocol + ".proxyHost", p.host());
setSystemPropertyIfUnset(p.protocol + ".proxyPort", String.valueOf(p.port()));
setSystemPropertyIfUnset(p.protocol + ".proxyUser", p.user());
setSystemPropertyIfUnset(p.protocol + ".proxyPassword", String.valueOf(p.password()));
setSystemPropertyIfUnset(
"http.nonProxyHosts",
p.nonProxyHosts()); // The HTTPS protocol handler will use the same nonProxyHosts
// property as the HTTP protocol. See here:
// https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/doc-files/net-properties.html#Proxies
}
}

return Arrays.stream(p.nonProxyHosts().split("\\|"))
.map(
nonProxyHost -> {
// If entry is "example.de", it should match example.de and *.example.de
if (!nonProxyHost.contains("*")) {
return "^(.*\\.)?" + Pattern.quote(nonProxyHost) + "$";
}

// Otherwise, process as wildcard domain
return nonProxyHost.replace(".", "\\.").replace("*", ".*");
})
.anyMatch(regex -> requestUri.matches(regex));
}

public HttpRoutePlanner getRoutePlanner(String protocol, HttpHost proxyHost) {
ProxyDetails p = proxyConfigForProtocols.get(protocol);

if (p != null && p.sourceIsSystemProperties()) {
LOGGER.debug("Using system default route planner for protocol: {}", protocol);
return new SystemDefaultRoutePlanner(
DefaultSchemePortResolver.INSTANCE, ProxySelector.getDefault());
} else if (proxyHost != null) {
LOGGER.debug(
"Using default proxy route planner for protocol: {} with proxy: {}", protocol, proxyHost);
return new DefaultProxyRoutePlanner(proxyHost);
private void setSystemPropertyIfUnset(String name, String value) {
if (System.getProperty(name) == null || System.getProperty(name).isBlank()) {
System.setProperty(name, value != null ? value : "");
}

LOGGER.debug("No proxy route planner used for protocol: {}", protocol);
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,12 @@ public static void setUp() {
Testcontainers.exposeHostPorts(proxy.port());
proxyContainer.withAccessToHost(true);
proxyContainer.start();
// Set up the HttpClient to use the proxy
String proxyHost = proxyContainer.getHost();
Integer proxyPort = proxyContainer.getMappedPort(3128);
setAllSystemProperties(proxyHost, proxyPort);
proxiedApacheHttpClient = CustomApacheHttpClient.create(HttpClients.custom());
}

private static void setAllSystemProperties(String proxyHost, Integer proxyPort) {
private static void setAllSystemProperties() {
String proxyHost = proxyContainer.getHost();
Integer proxyPort = proxyContainer.getMappedPort(3128);
System.setProperty("http.proxyHost", proxyHost);
System.setProperty("http.proxyPort", proxyPort.toString());
System.setProperty("http.nonProxyHosts", "");
Expand All @@ -208,20 +206,22 @@ private static void unsetAllSystemProperties() {

@AfterAll
public static void tearDown() {
proxyContainer.stop();
unsetAllSystemProperties();
proxyContainer.stop();
proxy.stop();
}

@AfterEach
@BeforeEach
public void resetProxy() {
unsetAllSystemProperties();
proxy.resetAll();
}

@Test
public void shouldReturn200_whenAuthenticationRequiredAndProvidedAsSystemProperty(
WireMockRuntimeInfo wmRuntimeInfo) {
proxy.stubFor(get("/protected").willReturn(ok().withBody("Hello, world!")));
setAllSystemProperties();

HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
Expand Down Expand Up @@ -261,7 +261,6 @@ public void shouldReturn200_whenValidEnvVar(
.execute(
() -> {
proxy.stubFor(get(path).willReturn(ok().withBody("Hello, world!")));
unsetAllSystemProperties();

HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
Expand All @@ -284,6 +283,7 @@ public void shouldReturn200_whenValidEnvVar(
shouldThrowException_whenAuthenticationRequiredAndNotProvidedOrInvalidAsSystemProperty(
String input, WireMockRuntimeInfo wmRuntimeInfo) {
proxy.stubFor(get("/protected").willReturn(ok().withBody("Hello, world!")));
setAllSystemProperties();
System.setProperty("http.proxyUser", input);
System.setProperty("http.proxyPassword", input);

Expand Down Expand Up @@ -313,7 +313,6 @@ public void shouldThrowException_whenAuthenticationRequiredAndNotProvidedAsEnvVa
.execute(
() -> {
proxy.stubFor(get("/protected").willReturn(ok().withBody("Hello, world!")));
unsetAllSystemProperties();
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
request.setUrl(getWireMockBaseUrlWithPath(wmRuntimeInfo, "/protected"));
Expand Down Expand Up @@ -343,6 +342,7 @@ public void shouldUseSystemProperties_WhenEnvVarAndSystemPropertiesAreProvided(
.execute(
() -> {
proxy.stubFor(get("/protected").willReturn(ok().withBody("Hello, world!")));
setAllSystemProperties();

HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
Expand All @@ -360,6 +360,7 @@ public void shouldUseSystemProperties_WhenEnvVarAndSystemPropertiesAreProvided(
@Test
public void shouldReturn200_whenGetAndProxySet(WireMockRuntimeInfo wmRuntimeInfo) {
proxy.stubFor(get("/path").willReturn(ok().withBody("Hello, world!")));
setAllSystemProperties();

HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.GET);
Expand All @@ -376,6 +377,7 @@ public void shouldReturn200_whenGetAndProxySet(WireMockRuntimeInfo wmRuntimeInfo
public void shouldReturn200_whenPostAndProxySet(WireMockRuntimeInfo wmRuntimeInfo) {
proxy.stubFor(
post("/path").willReturn(created().withJsonBody(new POJONode(Map.of("key1", "value1")))));
setAllSystemProperties();

HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.POST);
Expand All @@ -392,6 +394,7 @@ public void shouldReturn200_whenPostAndProxySet(WireMockRuntimeInfo wmRuntimeInf
public void shouldReturn200_whenPutAndProxySet(WireMockRuntimeInfo wmRuntimeInfo) {
proxy.stubFor(
put("/path").willReturn(ok().withJsonBody(new POJONode(Map.of("key1", "value1")))));
setAllSystemProperties();

HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.PUT);
Expand All @@ -407,6 +410,7 @@ public void shouldReturn200_whenPutAndProxySet(WireMockRuntimeInfo wmRuntimeInfo
@Test
public void shouldReturn200_whenDeleteAndProxySet(WireMockRuntimeInfo wmRuntimeInfo) {
proxy.stubFor(delete("/path").willReturn(noContent()));
setAllSystemProperties();

HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.DELETE);
Expand Down
Loading

0 comments on commit 9f9144c

Please sign in to comment.