Skip to content

Commit babd601

Browse files
committed
Handle TLS 1.3 bad cert errors in proxy handler
1 parent d376a94 commit babd601

File tree

9 files changed

+90
-18
lines changed

9 files changed

+90
-18
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "Netty NIO HTTP Client",
3+
"contributor": "",
4+
"type": "bugfix",
5+
"description": "Fixed the issue where BAD_CERTIFICATE issue manifested as acquire connection timeout error when using TLS1.3 and proxy."
6+
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ private void setupChannel(Channel ch, Promise<Channel> acquirePromise) {
121121
ch.pipeline().addLast(sslHandler);
122122
}
123123
ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, remoteAddress, tunnelEstablishedPromise));
124-
125124
tunnelEstablishedPromise.addListener((Future<Channel> f) -> {
126125
if (f.isSuccess()) {
127126
Channel tunnel = f.getNow();

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.LAST_HTTP_CONTENT_RECEIVED_KEY;
2323
import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.REQUEST_CONTEXT_KEY;
2424
import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.RESPONSE_COMPLETE_KEY;
25+
import static software.amazon.awssdk.http.nio.netty.internal.utils.NettyUtils.CLOSED_CHANNEL_MESSAGE;
2526

2627
import io.netty.buffer.ByteBuf;
2728
import io.netty.buffer.Unpooled;
@@ -310,7 +311,7 @@ private Throwable decorateException(Throwable originalCause) {
310311
} else if (originalCause instanceof WriteTimeoutException) {
311312
return new IOException("Write timed out", originalCause);
312313
} else if (originalCause instanceof ClosedChannelException) {
313-
return new IOException(getMessageForClosedChannel(), originalCause);
314+
return new IOException(CLOSED_CHANNEL_MESSAGE, originalCause);
314315
}
315316

316317
return originalCause;
@@ -369,12 +370,6 @@ private String getMessageForTooManyAcquireOperationsError() {
369370
+ "AWS, or by increasing the number of hosts sending requests.";
370371
}
371372

372-
private String getMessageForClosedChannel() {
373-
return "The channel was closed. This may have been done by the client (e.g. because the request was aborted), " +
374-
"by the service (e.g. because the request took too long or the client tried to write on a read-only socket), " +
375-
"or by an intermediary party (e.g. because the channel was idle for too long).";
376-
}
377-
378373
/**
379374
* Close and release the channel back to the pool.
380375
*

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@
3434
import java.util.function.Supplier;
3535
import software.amazon.awssdk.annotations.SdkInternalApi;
3636
import software.amazon.awssdk.annotations.SdkTestInternalApi;
37+
import software.amazon.awssdk.utils.Logger;
3738

3839
/**
3940
* Handler that initializes the HTTP tunnel.
4041
*/
4142
@SdkInternalApi
4243
public final class ProxyTunnelInitHandler extends ChannelDuplexHandler {
44+
public static final Logger log = Logger.loggerFor(ProxyTunnelInitHandler.class);
4345
private final ChannelPool sourcePool;
4446
private final URI remoteHost;
4547
private final Promise<Channel> initPromise;
@@ -65,9 +67,7 @@ public void handlerAdded(ChannelHandlerContext ctx) {
6567
HttpRequest connectRequest = connectRequest();
6668
ctx.channel().writeAndFlush(connectRequest).addListener(f -> {
6769
if (!f.isSuccess()) {
68-
ctx.close();
69-
sourcePool.release(ctx.channel());
70-
initPromise.setFailure(new IOException("Unable to send CONNECT request to proxy", f.cause()));
70+
handleConnectRequestFailure(ctx, f.cause());
7171
}
7272
});
7373
}
@@ -98,6 +98,40 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) {
9898
initPromise.setFailure(new IOException("Could not connect to proxy"));
9999
}
100100

101+
@Override
102+
public void channelInactive(ChannelHandlerContext ctx) {
103+
if (!initPromise.isDone()) {
104+
handleConnectRequestFailure(ctx, null);
105+
} else {
106+
log.debug(() -> "The proxy channel (" + ctx.channel().id() + ") is inactive");
107+
closeAndRelease(ctx);
108+
}
109+
}
110+
111+
@Override
112+
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
113+
if (!initPromise.isDone()) {
114+
handleConnectRequestFailure(ctx, cause);
115+
} else {
116+
log.debug(() -> "An exception occurred on the proxy tunnel channel (" + ctx.channel().id() + "). " +
117+
"The channel has been closed to prevent any ongoing issues.", cause);
118+
closeAndRelease(ctx);
119+
}
120+
}
121+
122+
private void handleConnectRequestFailure(ChannelHandlerContext ctx, Throwable cause) {
123+
closeAndRelease(ctx);
124+
String errorMsg = "Unable to send CONNECT request to proxy";
125+
IOException ioException = cause == null ? new IOException(errorMsg) :
126+
new IOException(errorMsg, cause);
127+
initPromise.setFailure(ioException);
128+
}
129+
130+
private void closeAndRelease(ChannelHandlerContext ctx) {
131+
ctx.close();
132+
sourcePool.release(ctx.channel());
133+
}
134+
101135
private HttpRequest connectRequest() {
102136
String uri = getUri();
103137
HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.CONNECT, uri,

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.RESPONSE_COMPLETE_KEY;
2525
import static software.amazon.awssdk.http.nio.netty.internal.utils.ExceptionHandlingUtils.tryCatch;
2626
import static software.amazon.awssdk.http.nio.netty.internal.utils.ExceptionHandlingUtils.tryCatchFinally;
27+
import static software.amazon.awssdk.http.nio.netty.internal.utils.NettyUtils.CLOSED_CHANNEL_MESSAGE;
2728

2829
import io.netty.buffer.ByteBuf;
2930
import io.netty.channel.Channel;
@@ -397,7 +398,7 @@ private void notifyIfResponseNotCompleted(ChannelHandlerContext handlerCtx) {
397398
handlerCtx.channel().attr(KEEP_ALIVE).set(false);
398399

399400
if (!Boolean.TRUE.equals(responseCompleted) && !Boolean.TRUE.equals(lastHttpContentReceived)) {
400-
IOException err = new IOException("Server failed to send complete response");
401+
IOException err = new IOException("Server failed to send complete response. " + CLOSED_CHANNEL_MESSAGE);
401402
runAndLogError("Fail to execute SdkAsyncHttpResponseHandler#onError", () -> requestCtx.handler().onError(err));
402403
executeFuture(handlerCtx).completeExceptionally(err);
403404
runAndLogError("Could not release channel", () -> closeAndRelease(handlerCtx));

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ public final class NettyUtils {
4040
*/
4141
public static final SucceededFuture<?> SUCCEEDED_FUTURE = new SucceededFuture<>(null, null);
4242

43+
// TODO: add a link to the guide on how to diagnose this error here once it's available
44+
public static final String CLOSED_CHANNEL_MESSAGE = "The channel was closed. This may have been done by the client (e.g. "
45+
+ "because the request was aborted), " +
46+
"by the service (e.g. because there was a handshake error, the request "
47+
+ "took too long, or the client tried to write on a read-only socket), " +
48+
"or by an intermediary party (e.g. because the channel was idle for too"
49+
+ " long).";
50+
4351
private static final Logger log = Logger.loggerFor(NettyUtils.class);
4452

4553
private NettyUtils() {

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,15 @@
2626
import com.github.tomakehurst.wiremock.WireMockServer;
2727
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
2828
import java.io.IOException;
29-
import org.hamcrest.CoreMatchers;
3029
import org.junit.After;
3130
import org.junit.AfterClass;
32-
import org.junit.Assume;
3331
import org.junit.BeforeClass;
3432
import org.junit.Rule;
3533
import org.junit.Test;
3634
import org.junit.rules.ExpectedException;
3735
import software.amazon.awssdk.http.EmptyPublisher;
3836
import software.amazon.awssdk.http.FileStoreTlsKeyManagersProvider;
37+
import software.amazon.awssdk.http.HttpTestUtils;
3938
import software.amazon.awssdk.http.SdkHttpFullRequest;
4039
import software.amazon.awssdk.http.SdkHttpMethod;
4140
import software.amazon.awssdk.http.TlsKeyManagersProvider;
@@ -139,8 +138,6 @@ public void proxyRequest_ableToAuthenticate() {
139138

140139
@Test
141140
public void proxyRequest_noKeyManagerGiven_notAbleToSendConnect() throws Throwable {
142-
// TODO: remove this and fix the issue with TLS1.3
143-
Assume.assumeThat(System.getProperty("java.version"), CoreMatchers.startsWith("1.8"));
144141
thrown.expectCause(instanceOf(IOException.class));
145142
thrown.expectMessage("Unable to send CONNECT request to proxy");
146143

@@ -173,6 +170,17 @@ public void proxyRequest_keyStoreSystemPropertiesConfigured_ableToAuthenticate()
173170
}
174171
}
175172

173+
@Test
174+
public void nonProxy_noKeyManagerGiven_shouldThrowException() {
175+
thrown.expectCause(instanceOf(IOException.class));
176+
thrown.expectMessage("The channel was closed");
177+
178+
netty = NettyNioAsyncHttpClient.builder()
179+
.buildWithDefaults(DEFAULTS);
180+
181+
HttpTestUtils.sendGetRequest(mockProxy.httpsPort(), netty).join();
182+
}
183+
176184
private void sendRequest(SdkAsyncHttpClient client, SdkAsyncHttpResponseHandler responseHandler) {
177185
AsyncExecuteRequest req = AsyncExecuteRequest.builder()
178186
.request(testSdkRequest())

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,15 +395,14 @@ public void builderUsesProvidedTrustManagersProvider() throws Exception {
395395

396396
SdkHttpRequest request = createRequest(uri);
397397
RecordingResponseHandler recorder = new RecordingResponseHandler();
398-
client.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(createProvider("")).responseHandler(recorder).build());
398+
netty.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(createProvider("")).responseHandler(recorder).build());
399399

400400
recorder.completeFuture.get(5, TimeUnit.SECONDS);
401401
} finally {
402402
selfSignedServer.stop();
403403
}
404404
}
405405

406-
407406
/**
408407
* Make a simple async request and wait for it to fiish.
409408
*

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import io.netty.handler.codec.http.HttpRequest;
3939
import io.netty.handler.codec.http.HttpResponseStatus;
4040
import io.netty.handler.codec.http.HttpVersion;
41+
import io.netty.handler.ssl.SslCloseCompletionEvent;
4142
import io.netty.handler.ssl.SslHandler;
4243
import io.netty.util.concurrent.Promise;
4344
import java.io.IOException;
@@ -161,6 +162,27 @@ public void requestWriteFails_failsPromise() {
161162
assertThat(promise.awaitUninterruptibly().isSuccess()).isFalse();
162163
}
163164

165+
@Test
166+
public void channelInactive_shouldFailPromise() throws Exception {
167+
Promise<Channel> promise = GROUP.next().newPromise();
168+
ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, REMOTE_HOST, promise);
169+
SslCloseCompletionEvent event = new SslCloseCompletionEvent(new RuntimeException(""));
170+
handler.channelInactive(mockCtx);
171+
172+
assertThat(promise.awaitUninterruptibly().isSuccess()).isFalse();
173+
verify(mockCtx).close();
174+
}
175+
176+
@Test
177+
public void unexpectedExceptionThrown_shouldFailPromise() throws Exception {
178+
Promise<Channel> promise = GROUP.next().newPromise();
179+
ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, REMOTE_HOST, promise);
180+
handler.exceptionCaught(mockCtx, new RuntimeException("exception"));
181+
182+
assertThat(promise.awaitUninterruptibly().isSuccess()).isFalse();
183+
verify(mockCtx).close();
184+
}
185+
164186
@Test
165187
public void handlerRemoved_removesCodec() {
166188
HttpClientCodec codec = new HttpClientCodec();

0 commit comments

Comments
 (0)