Skip to content

Commit 95e3ad9

Browse files
guillepb10next-guillermopriegocenedhryn
authored
Netty client support for authorized proxy (#2517)
* feat(client): support proxy with auth into netty client * test: test with proxy auth * doc: add javadoc to public interface and update changelog * doc: more javadoc * fix: fix pr comments * apply codestyle * fix: fix doc and equals into dto and codestyle * fix: fix typo and remove use of this into field access Co-authored-by: Guillermo Priego <[email protected]> Co-authored-by: Anna-Karin Salander <[email protected]>
1 parent bb1539b commit 95e3ad9

File tree

9 files changed

+305
-90
lines changed

9 files changed

+305
-90
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "guillepb10",
4+
"type": "feature",
5+
"description": "Add support for authenticated corporate proxies"
6+
}

http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,23 @@ public final class ProxyConfiguration implements ToCopyableBuilder<ProxyConfigur
3333
private final String scheme;
3434
private final String host;
3535
private final int port;
36+
private final String username;
37+
private final String password;
3638
private final Set<String> nonProxyHosts;
3739

3840
private ProxyConfiguration(BuilderImpl builder) {
3941
this.scheme = builder.scheme;
4042
this.host = builder.host;
4143
this.port = builder.port;
44+
this.username = builder.username;
45+
this.password = builder.password;
4246
this.nonProxyHosts = Collections.unmodifiableSet(builder.nonProxyHosts);
4347
}
4448

49+
public static Builder builder() {
50+
return new BuilderImpl();
51+
}
52+
4553
/**
4654
* @return The proxy scheme.
4755
*/
@@ -63,6 +71,20 @@ public int port() {
6371
return port;
6472
}
6573

74+
/**
75+
* @return The proxy username.
76+
*/
77+
public String username() {
78+
return username;
79+
}
80+
81+
/**
82+
* @return The proxy password.
83+
*/
84+
public String password() {
85+
return password;
86+
}
87+
6688
/**
6789
* @return The set of hosts that should not be proxied.
6890
*/
@@ -94,6 +116,14 @@ public boolean equals(Object o) {
94116
return false;
95117
}
96118

119+
if (username != null ? !username.equals(that.username) : that.username != null) {
120+
return false;
121+
}
122+
123+
if (password != null ? !password.equals(that.password) : that.password != null) {
124+
return false;
125+
}
126+
97127
return nonProxyHosts.equals(that.nonProxyHosts);
98128

99129
}
@@ -104,6 +134,8 @@ public int hashCode() {
104134
result = 31 * result + (host != null ? host.hashCode() : 0);
105135
result = 31 * result + port;
106136
result = 31 * result + nonProxyHosts.hashCode();
137+
result = 31 * result + (username != null ? username.hashCode() : 0);
138+
result = 31 * result + (password != null ? password.hashCode() : 0);
107139
return result;
108140
}
109141

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

115-
public static Builder builder() {
116-
return new BuilderImpl();
117-
}
118-
119147
/**
120148
* Builder for {@link ProxyConfiguration}.
121149
*/
122150
public interface Builder extends CopyableBuilder<Builder, ProxyConfiguration> {
123151

124152
/**
125153
* Set the hostname of the proxy.
154+
*
126155
* @param host The proxy host.
127156
* @return This object for method chaining.
128157
*/
129158
Builder host(String host);
130159

131160
/**
132161
* Set the port that the proxy expects connections on.
162+
*
133163
* @param port The proxy port.
134164
* @return This object for method chaining.
135165
*/
@@ -153,12 +183,30 @@ public interface Builder extends CopyableBuilder<Builder, ProxyConfiguration> {
153183
* @return This object for method chaining.
154184
*/
155185
Builder nonProxyHosts(Set<String> nonProxyHosts);
186+
187+
/**
188+
* Set the username used to authenticate with the proxy username.
189+
*
190+
* @param username The proxy username.
191+
* @return This object for method chaining.
192+
*/
193+
Builder username(String username);
194+
195+
/**
196+
* Set the password used to authenticate with the proxy password.
197+
*
198+
* @param password The proxy password.
199+
* @return This object for method chaining.
200+
*/
201+
Builder password(String password);
156202
}
157203

158204
private static final class BuilderImpl implements Builder {
159205
private String scheme;
160206
private String host;
161207
private int port;
208+
private String username;
209+
private String password;
162210
private Set<String> nonProxyHosts = Collections.emptySet();
163211

164212
private BuilderImpl() {
@@ -169,6 +217,8 @@ private BuilderImpl(ProxyConfiguration proxyConfiguration) {
169217
this.host = proxyConfiguration.host;
170218
this.port = proxyConfiguration.port;
171219
this.nonProxyHosts = new HashSet<>(proxyConfiguration.nonProxyHosts);
220+
this.username = proxyConfiguration.username;
221+
this.password = proxyConfiguration.password;
172222
}
173223

174224
@Override
@@ -189,6 +239,7 @@ public Builder port(int port) {
189239
return this;
190240
}
191241

242+
192243
@Override
193244
public Builder nonProxyHosts(Set<String> nonProxyHosts) {
194245
if (nonProxyHosts != null) {
@@ -199,6 +250,18 @@ public Builder nonProxyHosts(Set<String> nonProxyHosts) {
199250
return this;
200251
}
201252

253+
@Override
254+
public Builder username(String username) {
255+
this.username = username;
256+
return this;
257+
}
258+
259+
@Override
260+
public Builder password(String password) {
261+
this.password = password;
262+
return this;
263+
}
264+
202265
@Override
203266
public ProxyConfiguration build() {
204267
return new ProxyConfiguration(this);

http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,9 @@ protected SimpleChannelPoolAwareChannelPool newPool(URI key) {
136136
ChannelPool baseChannelPool;
137137
if (shouldUseProxyForHost(key)) {
138138
tcpChannelPool = new BetterSimpleChannelPool(bootstrap, NOOP_HANDLER);
139-
baseChannelPool = new Http1TunnelConnectionPool(bootstrap.config().group().next(), tcpChannelPool,
140-
sslContext, proxyAddress(key), key, pipelineInitializer);
139+
baseChannelPool = new Http1TunnelConnectionPool(bootstrap.config().group().next(), tcpChannelPool, sslContext,
140+
proxyAddress(key), proxyConfiguration.username(), proxyConfiguration.password(),
141+
key, pipelineInitializer);
141142
} else {
142143
tcpChannelPool = new BetterSimpleChannelPool(bootstrap, pipelineInitializer);
143144
baseChannelPool = tcpChannelPool;

http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,38 @@ public class Http1TunnelConnectionPool implements ChannelPool {
4949
private final ChannelPool delegate;
5050
private final SslContext sslContext;
5151
private final URI proxyAddress;
52+
private final String proxyUser;
53+
private final String proxyPassword;
5254
private final URI remoteAddress;
5355
private final ChannelPoolHandler handler;
5456
private final InitHandlerSupplier initHandlerSupplier;
5557

58+
public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext,
59+
URI proxyAddress, String proxyUsername, String proxyPassword,
60+
URI remoteAddress, ChannelPoolHandler handler) {
61+
this(eventLoop, delegate, sslContext,
62+
proxyAddress, proxyUsername, proxyPassword, remoteAddress, handler,
63+
ProxyTunnelInitHandler::new);
64+
}
65+
5666
public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext,
5767
URI proxyAddress, URI remoteAddress, ChannelPoolHandler handler) {
58-
this(eventLoop, delegate, sslContext, proxyAddress, remoteAddress, handler, ProxyTunnelInitHandler::new);
68+
this(eventLoop, delegate, sslContext,
69+
proxyAddress, null, null, remoteAddress, handler,
70+
ProxyTunnelInitHandler::new);
5971

6072
}
6173

6274
@SdkTestInternalApi
6375
Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext,
64-
URI proxyAddress, URI remoteAddress, ChannelPoolHandler handler,
65-
InitHandlerSupplier initHandlerSupplier) {
76+
URI proxyAddress, String proxyUser, String proxyPassword, URI remoteAddress,
77+
ChannelPoolHandler handler, InitHandlerSupplier initHandlerSupplier) {
6678
this.eventLoop = eventLoop;
6779
this.delegate = delegate;
6880
this.sslContext = sslContext;
6981
this.proxyAddress = proxyAddress;
82+
this.proxyUser = proxyUser;
83+
this.proxyPassword = proxyPassword;
7084
this.remoteAddress = remoteAddress;
7185
this.handler = handler;
7286
this.initHandlerSupplier = initHandlerSupplier;
@@ -120,7 +134,8 @@ private void setupChannel(Channel ch, Promise<Channel> acquirePromise) {
120134
if (sslHandler != null) {
121135
ch.pipeline().addLast(sslHandler);
122136
}
123-
ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, remoteAddress, tunnelEstablishedPromise));
137+
ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, proxyUser, proxyPassword, remoteAddress,
138+
tunnelEstablishedPromise));
124139
tunnelEstablishedPromise.addListener((Future<Channel> f) -> {
125140
if (f.isSuccess()) {
126141
Channel tunnel = f.getNow();
@@ -160,6 +175,7 @@ private static boolean isTunnelEstablished(Channel ch) {
160175
@SdkTestInternalApi
161176
@FunctionalInterface
162177
interface InitHandlerSupplier {
163-
ChannelHandler newInitHandler(ChannelPool sourcePool, URI remoteAddress, Promise<Channel> tunnelInitFuture);
178+
ChannelHandler newInitHandler(ChannelPool sourcePool, String proxyUsername, String proxyPassword, URI remoteAddress,
179+
Promise<Channel> tunnelInitFuture);
164180
}
165181
}

http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,35 +28,48 @@
2828
import io.netty.handler.codec.http.HttpRequest;
2929
import io.netty.handler.codec.http.HttpResponse;
3030
import io.netty.handler.codec.http.HttpVersion;
31+
import io.netty.util.CharsetUtil;
3132
import io.netty.util.concurrent.Promise;
3233
import java.io.IOException;
3334
import java.net.URI;
35+
import java.util.Base64;
3436
import java.util.function.Supplier;
3537
import software.amazon.awssdk.annotations.SdkInternalApi;
3638
import software.amazon.awssdk.annotations.SdkTestInternalApi;
3739
import software.amazon.awssdk.utils.Logger;
40+
import software.amazon.awssdk.utils.StringUtils;
3841

3942
/**
4043
* Handler that initializes the HTTP tunnel.
4144
*/
4245
@SdkInternalApi
4346
public final class ProxyTunnelInitHandler extends ChannelDuplexHandler {
47+
4448
public static final Logger log = Logger.loggerFor(ProxyTunnelInitHandler.class);
4549
private final ChannelPool sourcePool;
50+
private final String username;
51+
private final String password;
4652
private final URI remoteHost;
4753
private final Promise<Channel> initPromise;
4854
private final Supplier<HttpClientCodec> httpCodecSupplier;
4955

56+
public ProxyTunnelInitHandler(ChannelPool sourcePool, String proxyUsername, String proxyPassword, URI remoteHost,
57+
Promise<Channel> initPromise) {
58+
this(sourcePool, proxyUsername, proxyPassword, remoteHost, initPromise, HttpClientCodec::new);
59+
}
60+
5061
public ProxyTunnelInitHandler(ChannelPool sourcePool, URI remoteHost, Promise<Channel> initPromise) {
51-
this(sourcePool, remoteHost, initPromise, HttpClientCodec::new);
62+
this(sourcePool, null, null, remoteHost, initPromise, HttpClientCodec::new);
5263
}
5364

5465
@SdkTestInternalApi
55-
public ProxyTunnelInitHandler(ChannelPool sourcePool, URI remoteHost, Promise<Channel> initPromise,
56-
Supplier<HttpClientCodec> httpCodecSupplier) {
66+
public ProxyTunnelInitHandler(ChannelPool sourcePool, String prosyUsername, String proxyPassword,
67+
URI remoteHost, Promise<Channel> initPromise, Supplier<HttpClientCodec> httpCodecSupplier) {
5768
this.sourcePool = sourcePool;
5869
this.remoteHost = remoteHost;
5970
this.initPromise = initPromise;
71+
this.username = prosyUsername;
72+
this.password = proxyPassword;
6073
this.httpCodecSupplier = httpCodecSupplier;
6174
}
6275

@@ -137,6 +150,13 @@ private HttpRequest connectRequest() {
137150
HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.CONNECT, uri,
138151
Unpooled.EMPTY_BUFFER, false);
139152
request.headers().add(HttpHeaderNames.HOST, uri);
153+
154+
if (!StringUtils.isEmpty(this.username) && !StringUtils.isEmpty(this.password)) {
155+
String authToken = String.format("%s:%s", this.username, this.password);
156+
String authB64 = Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8));
157+
request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, String.format("Basic %s", authB64));
158+
}
159+
140160
return request;
141161
}
142162

http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package software.amazon.awssdk.http.nio.netty;
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
19+
1920
import java.lang.reflect.InvocationTargetException;
2021
import java.lang.reflect.Method;
2122
import java.util.HashSet;
@@ -47,8 +48,8 @@ public void toBuilder_roundTrip_producesExactCopy() {
4748
@Test
4849
public void setNonProxyHostsToNull_createsEmptySet() {
4950
ProxyConfiguration cfg = ProxyConfiguration.builder()
50-
.nonProxyHosts(null)
51-
.build();
51+
.nonProxyHosts(null)
52+
.build();
5253

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

6970
private ProxyConfiguration.Builder setAllPropertiesToRandomValues(ProxyConfiguration.Builder builder) {
7071
Stream.of(builder.getClass().getDeclaredMethods())
71-
.filter(m -> m.getParameterCount() == 1 && m.getReturnType().equals(ProxyConfiguration.Builder.class))
72-
.forEach(m -> {
73-
try {
74-
m.setAccessible(true);
75-
setRandomValue(builder, m);
76-
} catch (Exception e) {
77-
throw new RuntimeException("Could not create random proxy config", e);
78-
}
79-
});
72+
.filter(m -> m.getParameterCount() == 1 && m.getReturnType().equals(ProxyConfiguration.Builder.class))
73+
.forEach(m -> {
74+
try {
75+
m.setAccessible(true);
76+
setRandomValue(builder, m);
77+
} catch (Exception e) {
78+
throw new RuntimeException("Could not create random proxy config", e);
79+
}
80+
});
8081
return builder;
8182
}
8283

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

9798
private void verifyAllPropertiesSet(ProxyConfiguration cfg) {
9899
boolean hasNullProperty = Stream.of(cfg.getClass().getDeclaredMethods())
99-
.filter(m -> !m.getReturnType().equals(Void.class) && m.getParameterCount() == 0)
100-
.anyMatch(m -> {
101-
m.setAccessible(true);
102-
try {
103-
return m.invoke(cfg) == null;
104-
} catch (Exception e) {
105-
return true;
106-
}
107-
});
100+
.filter(m -> !m.getReturnType().equals(Void.class) && m.getParameterCount() == 0)
101+
.anyMatch(m -> {
102+
m.setAccessible(true);
103+
try {
104+
return m.invoke(cfg) == null;
105+
} catch (Exception e) {
106+
return true;
107+
}
108+
});
108109

109110
if (hasNullProperty) {
110111
throw new RuntimeException("Given configuration has unset property");

0 commit comments

Comments
 (0)