Skip to content

Netty client support for authorized proxy #2517

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 22 commits into from
Sep 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
157f838
feat(client): support proxy with auth into netty client
next-guillermopriego Jun 7, 2021
cf0b939
test: test with proxy auth
next-guillermopriego Jun 8, 2021
21bb134
doc: add javadoc to public interface and update changelog
next-guillermopriego Jun 8, 2021
63857b6
Merge branch 'master' into feature/support-proxy-with-auth
guillepb10 Jun 8, 2021
2852185
Merge branch 'master' into feature/support-proxy-with-auth
guillepb10 Jun 9, 2021
b590b58
doc: more javadoc
next-guillermopriego Jun 9, 2021
76c0422
Merge branch 'master' into feature/support-proxy-with-auth
guillepb10 Jun 25, 2021
dee2bb4
fix: fix pr comments
next-guillermopriego Jul 7, 2021
5d35750
apply codestyle
next-guillermopriego Jul 7, 2021
d9d3490
Merge branch 'feature/support-proxy-with-auth' of https://github.com/…
next-guillermopriego Jul 7, 2021
e977e01
Merge branch 'master' into feature/support-proxy-with-auth
guillepb10 Jul 7, 2021
7cae7f5
Merge branch 'master' into feature/support-proxy-with-auth
guillepb10 Jul 8, 2021
ab976fb
fix: fix doc and equals into dto and codestyle
next-guillermopriego Jul 9, 2021
a0a3b8a
Merge branch 'feature/support-proxy-with-auth' of https://github.com/…
next-guillermopriego Jul 9, 2021
216bbf9
Merge branch 'master' into feature/support-proxy-with-auth
guillepb10 Jul 9, 2021
ffd82d7
Merge branch 'master' into feature/support-proxy-with-auth
guillepb10 Jul 19, 2021
a2732c1
Merge branch 'master' into feature/support-proxy-with-auth
guillepb10 Jul 26, 2021
17df7a4
fix: fix typo and remove use of this into field access
next-guillermopriego Sep 6, 2021
623e019
Merge remote-tracking branch 'guille/feature/support-proxy-with-auth'…
next-guillermopriego Sep 6, 2021
3dde332
Merge branch 'master' into feature/support-proxy-with-auth
guillepb10 Sep 9, 2021
194ec44
Merge branch 'master' into feature/support-proxy-with-auth
cenedhryn Sep 9, 2021
b50d0f3
Merge branch 'master' into feature/support-proxy-with-auth
cenedhryn Sep 9, 2021
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-AWSSDKforJavav2-dd0c4ee.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"category": "AWS SDK for Java v2",
"contributor": "guillepb10",
"type": "feature",
"description": "Add support for authenticated corporate proxies"
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,23 @@ public final class ProxyConfiguration implements ToCopyableBuilder<ProxyConfigur
private final String scheme;
private final String host;
private final int port;
private final String username;
private final String password;
private final Set<String> nonProxyHosts;

private ProxyConfiguration(BuilderImpl builder) {
this.scheme = builder.scheme;
this.host = builder.host;
this.port = builder.port;
this.username = builder.username;
this.password = builder.password;
this.nonProxyHosts = Collections.unmodifiableSet(builder.nonProxyHosts);
}

public static Builder builder() {
return new BuilderImpl();
}

/**
* @return The proxy scheme.
*/
Expand All @@ -63,6 +71,20 @@ public int port() {
return port;
}

/**
* @return The proxy username.
*/
public String username() {
return username;
}

/**
* @return The proxy password.
*/
public String password() {
return password;
}

/**
* @return The set of hosts that should not be proxied.
*/
Expand Down Expand Up @@ -94,6 +116,14 @@ public boolean equals(Object o) {
return false;
}

if (username != null ? !username.equals(that.username) : that.username != null) {
return false;
}

if (password != null ? !password.equals(that.password) : that.password != null) {
return false;
}

return nonProxyHosts.equals(that.nonProxyHosts);

}
Expand All @@ -104,6 +134,8 @@ public int hashCode() {
result = 31 * result + (host != null ? host.hashCode() : 0);
result = 31 * result + port;
result = 31 * result + nonProxyHosts.hashCode();
result = 31 * result + (username != null ? username.hashCode() : 0);
result = 31 * result + (password != null ? password.hashCode() : 0);
return result;
}

Expand All @@ -112,24 +144,22 @@ public Builder toBuilder() {
return new BuilderImpl(this);
}

public static Builder builder() {
return new BuilderImpl();
}

/**
* Builder for {@link ProxyConfiguration}.
*/
public interface Builder extends CopyableBuilder<Builder, ProxyConfiguration> {

/**
* Set the hostname of the proxy.
*
* @param host The proxy host.
* @return This object for method chaining.
*/
Builder host(String host);

/**
* Set the port that the proxy expects connections on.
*
* @param port The proxy port.
* @return This object for method chaining.
*/
Expand All @@ -153,12 +183,30 @@ public interface Builder extends CopyableBuilder<Builder, ProxyConfiguration> {
* @return This object for method chaining.
*/
Builder nonProxyHosts(Set<String> nonProxyHosts);

/**
* Set the username used to authenticate with the proxy username.
*
* @param username The proxy username.
* @return This object for method chaining.
*/
Builder username(String username);

/**
* Set the password used to authenticate with the proxy password.
*
* @param password The proxy password.
* @return This object for method chaining.
*/
Builder password(String password);
}

private static final class BuilderImpl implements Builder {
private String scheme;
private String host;
private int port;
private String username;
private String password;
private Set<String> nonProxyHosts = Collections.emptySet();

private BuilderImpl() {
Expand All @@ -169,6 +217,8 @@ private BuilderImpl(ProxyConfiguration proxyConfiguration) {
this.host = proxyConfiguration.host;
this.port = proxyConfiguration.port;
this.nonProxyHosts = new HashSet<>(proxyConfiguration.nonProxyHosts);
this.username = proxyConfiguration.username;
this.password = proxyConfiguration.password;
}

@Override
Expand All @@ -189,6 +239,7 @@ public Builder port(int port) {
return this;
}


@Override
public Builder nonProxyHosts(Set<String> nonProxyHosts) {
if (nonProxyHosts != null) {
Expand All @@ -199,6 +250,18 @@ public Builder nonProxyHosts(Set<String> nonProxyHosts) {
return this;
}

@Override
public Builder username(String username) {
this.username = username;
return this;
}

@Override
public Builder password(String password) {
this.password = password;
return this;
}

@Override
public ProxyConfiguration build() {
return new ProxyConfiguration(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,9 @@ protected SimpleChannelPoolAwareChannelPool newPool(URI key) {
ChannelPool baseChannelPool;
if (shouldUseProxyForHost(key)) {
tcpChannelPool = new BetterSimpleChannelPool(bootstrap, NOOP_HANDLER);
baseChannelPool = new Http1TunnelConnectionPool(bootstrap.config().group().next(), tcpChannelPool,
sslContext, proxyAddress(key), key, pipelineInitializer);
baseChannelPool = new Http1TunnelConnectionPool(bootstrap.config().group().next(), tcpChannelPool, sslContext,
proxyAddress(key), proxyConfiguration.username(), proxyConfiguration.password(),
key, pipelineInitializer);
} else {
tcpChannelPool = new BetterSimpleChannelPool(bootstrap, pipelineInitializer);
baseChannelPool = tcpChannelPool;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,38 @@ public class Http1TunnelConnectionPool implements ChannelPool {
private final ChannelPool delegate;
private final SslContext sslContext;
private final URI proxyAddress;
private final String proxyUser;
private final String proxyPassword;
private final URI remoteAddress;
private final ChannelPoolHandler handler;
private final InitHandlerSupplier initHandlerSupplier;

public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext,
URI proxyAddress, String proxyUsername, String proxyPassword,
URI remoteAddress, ChannelPoolHandler handler) {
this(eventLoop, delegate, sslContext,
proxyAddress, proxyUsername, proxyPassword, remoteAddress, handler,
ProxyTunnelInitHandler::new);
}

public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext,
URI proxyAddress, URI remoteAddress, ChannelPoolHandler handler) {
this(eventLoop, delegate, sslContext, proxyAddress, remoteAddress, handler, ProxyTunnelInitHandler::new);
this(eventLoop, delegate, sslContext,
proxyAddress, null, null, remoteAddress, handler,
ProxyTunnelInitHandler::new);

}

@SdkTestInternalApi
Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext,
URI proxyAddress, URI remoteAddress, ChannelPoolHandler handler,
InitHandlerSupplier initHandlerSupplier) {
URI proxyAddress, String proxyUser, String proxyPassword, URI remoteAddress,
ChannelPoolHandler handler, InitHandlerSupplier initHandlerSupplier) {
this.eventLoop = eventLoop;
this.delegate = delegate;
this.sslContext = sslContext;
this.proxyAddress = proxyAddress;
this.proxyUser = proxyUser;
this.proxyPassword = proxyPassword;
this.remoteAddress = remoteAddress;
this.handler = handler;
this.initHandlerSupplier = initHandlerSupplier;
Expand Down Expand Up @@ -120,7 +134,8 @@ private void setupChannel(Channel ch, Promise<Channel> acquirePromise) {
if (sslHandler != null) {
ch.pipeline().addLast(sslHandler);
}
ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, remoteAddress, tunnelEstablishedPromise));
ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, proxyUser, proxyPassword, remoteAddress,
tunnelEstablishedPromise));
tunnelEstablishedPromise.addListener((Future<Channel> f) -> {
if (f.isSuccess()) {
Channel tunnel = f.getNow();
Expand Down Expand Up @@ -160,6 +175,7 @@ private static boolean isTunnelEstablished(Channel ch) {
@SdkTestInternalApi
@FunctionalInterface
interface InitHandlerSupplier {
ChannelHandler newInitHandler(ChannelPool sourcePool, URI remoteAddress, Promise<Channel> tunnelInitFuture);
ChannelHandler newInitHandler(ChannelPool sourcePool, String proxyUsername, String proxyPassword, URI remoteAddress,
Promise<Channel> tunnelInitFuture);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,35 +28,48 @@
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.Promise;
import java.io.IOException;
import java.net.URI;
import java.util.Base64;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
import software.amazon.awssdk.utils.Logger;
import software.amazon.awssdk.utils.StringUtils;

/**
* Handler that initializes the HTTP tunnel.
*/
@SdkInternalApi
public final class ProxyTunnelInitHandler extends ChannelDuplexHandler {

public static final Logger log = Logger.loggerFor(ProxyTunnelInitHandler.class);
private final ChannelPool sourcePool;
private final String username;
private final String password;
private final URI remoteHost;
private final Promise<Channel> initPromise;
private final Supplier<HttpClientCodec> httpCodecSupplier;

public ProxyTunnelInitHandler(ChannelPool sourcePool, String proxyUsername, String proxyPassword, URI remoteHost,
Promise<Channel> initPromise) {
this(sourcePool, proxyUsername, proxyPassword, remoteHost, initPromise, HttpClientCodec::new);
}

public ProxyTunnelInitHandler(ChannelPool sourcePool, URI remoteHost, Promise<Channel> initPromise) {
this(sourcePool, remoteHost, initPromise, HttpClientCodec::new);
this(sourcePool, null, null, remoteHost, initPromise, HttpClientCodec::new);
}

@SdkTestInternalApi
public ProxyTunnelInitHandler(ChannelPool sourcePool, URI remoteHost, Promise<Channel> initPromise,
Supplier<HttpClientCodec> httpCodecSupplier) {
public ProxyTunnelInitHandler(ChannelPool sourcePool, String prosyUsername, String proxyPassword,
URI remoteHost, Promise<Channel> initPromise, Supplier<HttpClientCodec> httpCodecSupplier) {
this.sourcePool = sourcePool;
this.remoteHost = remoteHost;
this.initPromise = initPromise;
this.username = prosyUsername;
this.password = proxyPassword;
this.httpCodecSupplier = httpCodecSupplier;
}

Expand Down Expand Up @@ -137,6 +150,13 @@ private HttpRequest connectRequest() {
HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.CONNECT, uri,
Unpooled.EMPTY_BUFFER, false);
request.headers().add(HttpHeaderNames.HOST, uri);

if (!StringUtils.isEmpty(this.username) && !StringUtils.isEmpty(this.password)) {
String authToken = String.format("%s:%s", this.username, this.password);
String authB64 = Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8));
request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, String.format("Basic %s", authB64));
}

return request;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package software.amazon.awssdk.http.nio.netty;

import static org.assertj.core.api.Assertions.assertThat;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
Expand Down Expand Up @@ -47,8 +48,8 @@ public void toBuilder_roundTrip_producesExactCopy() {
@Test
public void setNonProxyHostsToNull_createsEmptySet() {
ProxyConfiguration cfg = ProxyConfiguration.builder()
.nonProxyHosts(null)
.build();
.nonProxyHosts(null)
.build();

assertThat(cfg.nonProxyHosts()).isEmpty();
}
Expand All @@ -68,15 +69,15 @@ private ProxyConfiguration allPropertiesSetConfig() {

private ProxyConfiguration.Builder setAllPropertiesToRandomValues(ProxyConfiguration.Builder builder) {
Stream.of(builder.getClass().getDeclaredMethods())
.filter(m -> m.getParameterCount() == 1 && m.getReturnType().equals(ProxyConfiguration.Builder.class))
.forEach(m -> {
try {
m.setAccessible(true);
setRandomValue(builder, m);
} catch (Exception e) {
throw new RuntimeException("Could not create random proxy config", e);
}
});
.filter(m -> m.getParameterCount() == 1 && m.getReturnType().equals(ProxyConfiguration.Builder.class))
.forEach(m -> {
try {
m.setAccessible(true);
setRandomValue(builder, m);
} catch (Exception e) {
throw new RuntimeException("Could not create random proxy config", e);
}
});
return builder;
}

Expand All @@ -96,15 +97,15 @@ private void setRandomValue(Object o, Method setter) throws InvocationTargetExce

private void verifyAllPropertiesSet(ProxyConfiguration cfg) {
boolean hasNullProperty = Stream.of(cfg.getClass().getDeclaredMethods())
.filter(m -> !m.getReturnType().equals(Void.class) && m.getParameterCount() == 0)
.anyMatch(m -> {
m.setAccessible(true);
try {
return m.invoke(cfg) == null;
} catch (Exception e) {
return true;
}
});
.filter(m -> !m.getReturnType().equals(Void.class) && m.getParameterCount() == 0)
.anyMatch(m -> {
m.setAccessible(true);
try {
return m.invoke(cfg) == null;
} catch (Exception e) {
return true;
}
});

if (hasNullProperty) {
throw new RuntimeException("Given configuration has unset property");
Expand Down
Loading