Skip to content

Commit 07322d2

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

File tree

8 files changed

+79
-17
lines changed

8 files changed

+79
-17
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/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: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,7 @@ public void handlerAdded(ChannelHandlerContext ctx) {
6565
HttpRequest connectRequest = connectRequest();
6666
ctx.channel().writeAndFlush(connectRequest).addListener(f -> {
6767
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()));
68+
handleConnectRequestFailure(ctx, f.cause());
7169
}
7270
});
7371
}
@@ -98,6 +96,31 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) {
9896
initPromise.setFailure(new IOException("Could not connect to proxy"));
9997
}
10098

99+
@Override
100+
public void channelInactive(ChannelHandlerContext ctx) {
101+
if (!initPromise.isDone()) {
102+
handleConnectRequestFailure(ctx, null);
103+
}
104+
}
105+
106+
@Override
107+
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
108+
if (!initPromise.isDone()) {
109+
handleConnectRequestFailure(ctx, cause);
110+
} else {
111+
ctx.fireExceptionCaught(cause);
112+
}
113+
}
114+
115+
private void handleConnectRequestFailure(ChannelHandlerContext ctx, Throwable cause) {
116+
ctx.close();
117+
sourcePool.release(ctx.channel());
118+
String errorMsg = "Unable to send CONNECT request to proxy";
119+
IOException ioException = cause == null ? new IOException(errorMsg) :
120+
new IOException(errorMsg, cause);
121+
initPromise.setFailure(ioException);
122+
}
123+
101124
private HttpRequest connectRequest() {
102125
String uri = getUri();
103126
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+
public static final String CLOSED_CHANNEL_MESSAGE = "The channel was closed. This may have been done by the client (e.g. "
44+
+ "because the request was aborted), " +
45+
"by the service (e.g. because there was a handshake error[use -Djavax"
46+
+ ".net.debug=ssl to enable SSL logs], 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)