Skip to content

Support Non proxy host settings in the ProxyConfiguration for Crt http client. #4962

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 27, 2024
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
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSCRTHTTPClient-69af591.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "AWS CRT HTTP Client",
"contributor": "",
"description": "Support Non proxy host settings in the ProxyConfiguration for Crt http client."
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@

package software.amazon.awssdk.crtcore;

import static software.amazon.awssdk.utils.StringUtils.lowerCase;

import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.crt.http.HttpMonitoringOptions;
import software.amazon.awssdk.crt.http.HttpProxyOptions;
Expand All @@ -33,7 +37,9 @@ public static Optional<HttpProxyOptions> resolveProxy(CrtProxyConfiguration prox
if (proxyConfiguration == null) {
return Optional.empty();
}

if (doesTargetMatchNonProxyHosts(proxyConfiguration.host(), proxyConfiguration.nonProxyHosts())) {
return Optional.empty();
}
HttpProxyOptions clientProxyOptions = new HttpProxyOptions();

clientProxyOptions.setHost(proxyConfiguration.host());
Expand All @@ -54,11 +60,19 @@ public static Optional<HttpProxyOptions> resolveProxy(CrtProxyConfiguration prox
return Optional.of(clientProxyOptions);
}

private static boolean doesTargetMatchNonProxyHosts(String target, Set<String> hostPatterns) {
return Optional.ofNullable(hostPatterns)
.map(patterns ->
patterns.stream()
.filter(Objects::nonNull)
.anyMatch(pattern -> target != null && lowerCase(target).matches(pattern)))
.orElse(false);
}

public static Optional<HttpMonitoringOptions> resolveHttpMonitoringOptions(CrtConnectionHealthConfiguration config) {
if (config == null) {
return Optional.empty();
}

HttpMonitoringOptions httpMonitoringOptions = new HttpMonitoringOptions();
httpMonitoringOptions.setMinThroughputBytesPerSecond(config.minimumThroughputInBps());
int seconds = NumericUtils.saturatedCast(config.minimumThroughputTimeout().getSeconds());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@

import static software.amazon.awssdk.utils.ProxyConfigProvider.fromSystemEnvironmentSettings;

import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.utils.ProxyConfigProvider;
import software.amazon.awssdk.utils.ProxySystemSetting;
Expand All @@ -36,6 +39,7 @@ public abstract class CrtProxyConfiguration {
private final String password;
private final Boolean useSystemPropertyValues;
private final Boolean useEnvironmentVariableValues;
private final Set<String> nonProxyHosts;

protected CrtProxyConfiguration(DefaultBuilder<?> builder) {
this.useSystemPropertyValues = builder.useSystemPropertyValues;
Expand All @@ -49,6 +53,7 @@ protected CrtProxyConfiguration(DefaultBuilder<?> builder) {
this.port = resolvePort(builder, proxyConfigProvider);
this.username = resolveUsername(builder, proxyConfigProvider);
this.password = resolvePassword(builder, proxyConfigProvider);
this.nonProxyHosts = resolveNonProxyHosts(builder, proxyConfigProvider);
}

private static String resolvePassword(DefaultBuilder<?> builder, ProxyConfigProvider proxyConfigProvider) {
Expand Down Expand Up @@ -83,6 +88,13 @@ private static String resolveHost(DefaultBuilder<?> builder, ProxyConfigProvider
}
}

private Set<String> resolveNonProxyHosts(DefaultBuilder<?> builder, ProxyConfigProvider proxyConfigProvider) {
if (builder.nonProxyHosts != null || proxyConfigProvider == null) {
return builder.nonProxyHosts;
}
return proxyConfigProvider.nonProxyHosts();
}

/**
* @return The proxy scheme.
*/
Expand Down Expand Up @@ -132,6 +144,16 @@ public final Boolean isUseEnvironmentVariableValues() {
return useEnvironmentVariableValues;
}

/**
* Retrieves the hosts that the client is allowed to access without going through the proxy.
* If the value is not set on the object, the value represented by the environment variable or system property is returned.
*
* @see Builder#nonProxyHosts(Set)
*/
public Set<String> nonProxyHosts() {
return Collections.unmodifiableSet(nonProxyHosts != null ? nonProxyHosts : Collections.emptySet());
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down Expand Up @@ -162,7 +184,10 @@ public boolean equals(Object o) {
if (!Objects.equals(useSystemPropertyValues, that.useSystemPropertyValues)) {
return false;
}
return Objects.equals(useEnvironmentVariableValues, that.useEnvironmentVariableValues);
if (!Objects.equals(useEnvironmentVariableValues, that.useEnvironmentVariableValues)) {
return false;
}
return Objects.equals(nonProxyHosts, that.nonProxyHosts);
}

@Override
Expand All @@ -175,6 +200,7 @@ public int hashCode() {
result = 31 * result + (useSystemPropertyValues != null ? useSystemPropertyValues.hashCode() : 0);
result = 31 * result + (useEnvironmentVariableValues != null ? useEnvironmentVariableValues.hashCode() : 0);
result = 31 * result + (scheme != null ? scheme.hashCode() : 0);
result = 31 * result + (nonProxyHosts != null ? nonProxyHosts.hashCode() : 0);
return result;
}

Expand Down Expand Up @@ -253,6 +279,17 @@ public interface Builder {
*/
Builder useEnvironmentVariableValues(Boolean useEnvironmentVariableValues);

/**
* Configure the hosts that the client is allowed to access without going through the proxy.
*/
Builder nonProxyHosts(Set<String> nonProxyHosts);


/**
* Add a host that the client is allowed to access without going through the proxy.
*/
Builder addNonProxyHost(String nonProxyHost);


CrtProxyConfiguration build();
}
Expand All @@ -266,6 +303,8 @@ protected abstract static class DefaultBuilder<B extends Builder> implements Bui
private String password;
private Boolean useSystemPropertyValues = Boolean.TRUE;
private Boolean useEnvironmentVariableValues = Boolean.TRUE;
private Set<String> nonProxyHosts;


protected DefaultBuilder() {
}
Expand All @@ -278,6 +317,7 @@ protected DefaultBuilder(CrtProxyConfiguration proxyConfiguration) {
this.port = proxyConfiguration.port;
this.username = proxyConfiguration.username;
this.password = proxyConfiguration.password;
this.nonProxyHosts = proxyConfiguration.nonProxyHosts;
}

@Override
Expand Down Expand Up @@ -322,6 +362,21 @@ public B useEnvironmentVariableValues(Boolean useEnvironmentVariableValues) {
return (B) this;
}

@Override
public B nonProxyHosts(Set<String> nonProxyHosts) {
this.nonProxyHosts = nonProxyHosts != null ? new HashSet<>(nonProxyHosts) : null;
return (B) this;
}

@Override
public B addNonProxyHost(String nonProxyHost) {
if (this.nonProxyHosts == null) {
this.nonProxyHosts = new HashSet<>();
}
this.nonProxyHosts.add(nonProxyHost);
return (B) this;
}

public B setuseEnvironmentVariableValues(Boolean useEnvironmentVariableValues) {
return useEnvironmentVariableValues(useEnvironmentVariableValues);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@

import java.time.Duration;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import software.amazon.awssdk.crt.http.HttpMonitoringOptions;
import software.amazon.awssdk.crt.http.HttpProxyOptions;
Expand Down Expand Up @@ -54,6 +58,60 @@ void resolveProxy_emptyProxy_shouldReturnEmpty() {
assertThat(CrtConfigurationUtils.resolveProxy(null, tlsContext)).isEmpty();
}

@ParameterizedTest
@ValueSource(strings = {".*?.2.3.4", "1.*?.3.4", ".*?"})
void resolveProxy_withSingleNonProxyHostsWidCards_shouldReturnEmpty(String nonProxyHost) {
TlsContext tlsContext = Mockito.mock(TlsContext.class);
CrtProxyConfiguration configuration = new TestProxy.Builder().host("1.2.3.4")
.port(123)
.scheme("https")
.password("bar")
.username("foo")
.nonProxyHosts(Stream.of(nonProxyHost,"someRandom")
.collect(Collectors.toSet()))
.build();
assertThat(CrtConfigurationUtils.resolveProxy(configuration, tlsContext)).isEmpty();
}



@Test
void resolveProxy_withNullHostAndNonPorxy_shouldNotReturnEmpty( ) {
TlsContext tlsContext = Mockito.mock(TlsContext.class);
CrtProxyConfiguration configuration = new TestProxy.Builder().host(null)
.port(123)
.scheme("https")
.password("bar")
.username("foo")
.nonProxyHosts(Stream.of("someRandom", "null")
.collect(Collectors.toSet()))
.build();
assertThat(CrtConfigurationUtils.resolveProxy(configuration, tlsContext)).isNotEmpty();
}

@Test
void resolveProxy_basicAuthorization_WithNonMatchingNoProxy() {
CrtProxyConfiguration configuration = new TestProxy.Builder().host("1.2.3.4")
.port(123)
.scheme("https")
.password("bar")
.addNonProxyHost("someRandom")
.addNonProxyHost(null)
.username("foo")
.build();

TlsContext tlsContext = Mockito.mock(TlsContext.class);

Optional<HttpProxyOptions> httpProxyOptions = CrtConfigurationUtils.resolveProxy(configuration, tlsContext);
assertThat(httpProxyOptions).hasValueSatisfying(proxy -> {
assertThat(proxy.getTlsContext()).isEqualTo(tlsContext);
assertThat(proxy.getAuthorizationPassword()).isEqualTo("bar");
assertThat(proxy.getAuthorizationUsername()).isEqualTo("foo");
assertThat(proxy.getAuthorizationType()).isEqualTo(HttpProxyOptions.HttpProxyAuthorizationType.Basic);
});
}


@Test
void resolveProxy_noneAuthorization() {
CrtProxyConfiguration configuration = new TestProxy.Builder().host("1.2.3.4")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@

package software.amazon.awssdk.http.crt;

import java.util.Set;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.crtcore.CrtProxyConfiguration;
import software.amazon.awssdk.utils.ProxyEnvironmentSetting;
import software.amazon.awssdk.utils.ProxySystemSetting;
import software.amazon.awssdk.utils.builder.CopyableBuilder;
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
Expand Down Expand Up @@ -116,9 +118,35 @@ public interface Builder extends CrtProxyConfiguration.Builder, CopyableBuilder<
Builder useSystemPropertyValues(Boolean useSystemPropertyValues);


/**
* Set the option whether to use environment variable values for {@link ProxyEnvironmentSetting} if any of the config
* options are missing. The value is set to "true" by default, enabling the SDK to automatically use environment variable
* values for proxy configuration options that are not provided during building the {@link ProxyConfiguration} object. To
* disable this behavior, set this value to "false".It is important to note that when this property is set to "true," all
* proxy settings will exclusively originate from Environment Variable Values, and no partial settings will be obtained
* from System Property Values.
* <p>Comma-separated host names in the NO_PROXY environment variable indicate multiple hosts to exclude from
* proxy settings.
*
* @param useEnvironmentVariableValues The option whether to use environment variable values
* @return This object for method chaining.
*/
@Override
Builder useEnvironmentVariableValues(Boolean useEnvironmentVariableValues);

/**
* Configure the hosts that the client is allowed to access without going through the proxy.
*/
@Override
Builder nonProxyHosts(Set<String> nonProxyHosts);


/**
* Add a host that the client is allowed to access without going through the proxy.
*/
@Override
Builder addNonProxyHost(String nonProxyHost);

@Override
ProxyConfiguration build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.assertj.core.api.Assertions.assertThat;

import java.net.URISyntaxException;
import java.util.Set;
import software.amazon.awssdk.http.HttpProxyTestSuite;
import software.amazon.awssdk.http.proxy.TestProxySetting;

Expand All @@ -35,6 +36,7 @@ protected void assertProxyConfiguration(TestProxySetting userSetProxySettings,
Integer portNumber = userSetProxySettings.getPort();
String userName = userSetProxySettings.getUserName();
String password = userSetProxySettings.getPassword();
Set<String> nonProxyHosts = userSetProxySettings.getNonProxyHosts();

if (hostName != null) {
proxyBuilder.host(hostName);
Expand All @@ -48,6 +50,9 @@ protected void assertProxyConfiguration(TestProxySetting userSetProxySettings,
if (password != null) {
proxyBuilder.password(password);
}
if (nonProxyHosts != null && !nonProxyHosts.isEmpty()) {
proxyBuilder.nonProxyHosts(nonProxyHosts);
}
}

if (!"http".equals(protocol)) {
Expand All @@ -64,6 +69,7 @@ protected void assertProxyConfiguration(TestProxySetting userSetProxySettings,
assertThat(proxyConfiguration.port()).isEqualTo(expectedProxySettings.getPort());
assertThat(proxyConfiguration.username()).isEqualTo(expectedProxySettings.getUserName());
assertThat(proxyConfiguration.password()).isEqualTo(expectedProxySettings.getPassword());
assertThat(proxyConfiguration.nonProxyHosts()).isEqualTo(expectedProxySettings.getNonProxyHosts());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -181,6 +185,8 @@ private void setRandomValue(Object o, Method setter) throws InvocationTargetExce
setter.invoke(o, RNG.nextInt());
} else if (Boolean.class.equals(paramClass)) {
setter.invoke(o, RNG.nextBoolean());
} else if (Set.class.equals(paramClass)) {
setter.invoke(o, IntStream.range(0, 5).mapToObj(i -> randomString()).collect(Collectors.toSet()));
} else {
throw new RuntimeException("Don't know how create random value for type " + paramClass);
}
Expand Down
Loading