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 7 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 autheticated corporate proxies"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

autheticated -> authenticated

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,16 @@ 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);
}

Expand All @@ -63,6 +67,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 @@ -153,12 +171,28 @@ 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 server.
* @param username The proxy username.
* @return This object for method chaining.
*/
Builder username(String username);

/**
* Set the password used to authenticate with the proxy server.
* @param password The proxy username.
* @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 Down Expand Up @@ -189,6 +223,8 @@ public Builder port(int port) {
return this;
}



@Override
public Builder nonProxyHosts(Set<String> nonProxyHosts) {
if (nonProxyHosts != null) {
Expand All @@ -199,6 +235,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, this.proxyUser, this.proxyPassword, remoteAddress,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not against using this but this module should be consistent in its usage of class members and currently this isn't used widely, so let's remove it for readability.

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 @@ -24,15 +24,15 @@
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.TLS_KEY_MANAGERS_PROVIDER;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.github.tomakehurst.wiremock.junit.WireMockRule;

import io.netty.util.CharsetUtil;
import org.apache.commons.lang3.CharSetUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.After;
import org.junit.Rule;
Expand Down Expand Up @@ -215,6 +215,39 @@ public void usingProxy_noSchemeGiven_defaultsToHttp() {
assertThat(requests).contains("CONNECT some-awesome-service:443");
}

@Test
public void usingProxy_withAuth() {
ProxyConfiguration proxyConfiguration = ProxyConfiguration.builder()
.host("localhost")
.port(mockProxy.port())
.username("myuser")
.password("mypassword")
.build();

channelPoolMap = AwaitCloseChannelPoolMap.builder()
.proxyConfiguration(proxyConfiguration)
.sdkChannelOptions(new SdkChannelOptions())
.sdkEventLoopGroup(SdkEventLoopGroup.builder().build())
.configuration(new NettyConfiguration(GLOBAL_HTTP_DEFAULTS))
.protocol(Protocol.HTTP1_1)
.maxStreams(100)
.sslProvider(SslProvider.OPENSSL)
.build();

SimpleChannelPoolAwareChannelPool simpleChannelPoolAwareChannelPool = channelPoolMap.newPool(
URI.create("https://some-awesome-service:443"));

simpleChannelPoolAwareChannelPool.acquire().awaitUninterruptibly();

String requests = recorder.requests().toString();

assertThat(requests).contains("CONNECT some-awesome-service:443");

String authB64 = Base64.getEncoder().encodeToString("myuser:mypassword".getBytes(CharsetUtil.UTF_8));
String authHeaderValue = String.format("Basic %s", authB64);
assertThat(requests).contains(String.format("proxy-authorization: %s", authHeaderValue));
}

@Test
public void usesProvidedKeyManagersProvider() {
TlsKeyManagersProvider provider = mock(TlsKeyManagersProvider.class);
Expand Down
Loading