Skip to content

Commit f67ef63

Browse files
authored
feat(aws-crt-client): add tlsNegotiationTimeout configuration (#7075)
* feat(aws-crt-client): add tlsNegotiationTimeout configuration Add tlsNegotiationTimeout(Duration) to AwsCrtAsyncHttpClient.Builder and AwsCrtHttpClient.Builder, mirroring the Netty client. Configures the maximum TLS handshake duration (CLIENT HELLO through key exchange). Defaults to 10 seconds, matching the underlying CRT runtime's native default (AWS_DEFAULT_TLS_TIMEOUT_MS). * Fix tests * fix(aws-crt-client): set SNI server name on TLS conn options All HTTPS requests via AwsCrtHttpClient and AwsCrtAsyncHttpClient on Linux (s2n-tls) failed with AWS_IO_TLS_ERROR_NEGOTIATION_FAILURE. Switching the connection pool from withTlsContext(tlsContext) to withTlsConnectionOptions(tlsConnectionOptions) dropped the SNI server name: the native connection manager only auto-derives server_name from the URI on the tls_ctx path, not on the tls_connection_options path. s2n then validated the peer certificate against a null hostname and rejected the handshake. Build the TlsConnectionOptions per pool from the pool's URI host so SNI and x509 hostname verification succeed.
1 parent 8b126b7 commit f67ef63

14 files changed

Lines changed: 574 additions & 45 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS Common Runtime HTTP Client",
4+
"contributor": "",
5+
"description": "Added tlsNegotiationTimeout(Duration) configuration to AwsCrtAsyncHttpClient and AwsCrtHttpClient builders, mirroring the option on the Netty client. Configures the maximum amount of time a TLS handshake may take, from CLIENT HELLO through key exchange. Defaults to 10 seconds, matching the underlying CRT runtime's native default."
6+
}

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,17 @@ AwsCrtAsyncHttpClient.Builder connectionHealthConfiguration(Consumer<ConnectionH
194194
*/
195195
AwsCrtAsyncHttpClient.Builder connectionAcquisitionTimeout(Duration connectionAcquisitionTimeout);
196196

197+
/**
198+
* Configure the maximum amount of time that a TLS handshake is allowed to take from the time the CLIENT HELLO
199+
* message is sent to the time the client and server have fully negotiated ciphers and exchanged keys.
200+
*
201+
* <p>By default, it's 10 seconds.
202+
*
203+
* @param tlsNegotiationTimeout the timeout duration; must be positive
204+
* @return this builder for method chaining.
205+
*/
206+
AwsCrtAsyncHttpClient.Builder tlsNegotiationTimeout(Duration tlsNegotiationTimeout);
207+
197208
/**
198209
* Configure whether to enable {@code tcpKeepAlive} and relevant configuration for all connections established by this
199210
* client.
@@ -267,13 +278,15 @@ public Builder protocol(Protocol protocol) {
267278
@Override
268279
public SdkAsyncHttpClient build() {
269280
return new AwsCrtAsyncHttpClient(this, getAttributeMap().build()
281+
.merge(AwsCrtHttpClientBase.AWS_CRT_HTTP_DEFAULTS)
270282
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
271283
}
272284

273285
@Override
274286
public SdkAsyncHttpClient buildWithDefaults(AttributeMap serviceDefaults) {
275287
return new AwsCrtAsyncHttpClient(this, getAttributeMap().build()
276288
.merge(serviceDefaults)
289+
.merge(AwsCrtHttpClientBase.AWS_CRT_HTTP_DEFAULTS)
277290
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
278291
}
279292

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,17 @@ AwsCrtHttpClient.Builder connectionHealthConfiguration(Consumer<ConnectionHealth
246246
*/
247247
AwsCrtHttpClient.Builder connectionAcquisitionTimeout(Duration connectionAcquisitionTimeout);
248248

249+
/**
250+
* Configure the maximum amount of time that a TLS handshake is allowed to take from the time the CLIENT HELLO
251+
* message is sent to the time the client and server have fully negotiated ciphers and exchanged keys.
252+
*
253+
* <p>By default, it's 10 seconds.
254+
*
255+
* @param tlsNegotiationTimeout the timeout duration; must be positive
256+
* @return this builder for method chaining.
257+
*/
258+
AwsCrtHttpClient.Builder tlsNegotiationTimeout(Duration tlsNegotiationTimeout);
259+
249260
/**
250261
* Configure whether to enable {@code tcpKeepAlive} and relevant configuration for all connections established by this
251262
* client.
@@ -305,13 +316,15 @@ private static final class DefaultBuilder
305316
@Override
306317
public AwsCrtHttpClient build() {
307318
return new AwsCrtHttpClient(this, getAttributeMap().build()
319+
.merge(AwsCrtHttpClientBase.AWS_CRT_HTTP_DEFAULTS)
308320
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
309321
}
310322

311323
@Override
312324
public AwsCrtHttpClient buildWithDefaults(AttributeMap serviceDefaults) {
313325
return new AwsCrtHttpClient(this, getAttributeMap().build()
314326
.merge(serviceDefaults)
327+
.merge(AwsCrtHttpClientBase.AWS_CRT_HTTP_DEFAULTS)
315328
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
316329
}
317330
}

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@
1919
import static software.amazon.awssdk.crtcore.CrtConfigurationUtils.resolveProxy;
2020
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.PROTOCOL;
2121
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildSocketOptions;
22+
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildTlsConnectionOptions;
2223
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveCipherPreference;
2324
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
2425

2526
import java.net.URI;
27+
import java.time.Duration;
2628
import java.util.LinkedList;
2729
import java.util.Map;
2830
import java.util.concurrent.ConcurrentHashMap;
2931
import software.amazon.awssdk.annotations.SdkProtectedApi;
32+
import software.amazon.awssdk.annotations.SdkTestInternalApi;
3033
import software.amazon.awssdk.crt.CrtResource;
3134
import software.amazon.awssdk.crt.http.Http2StreamManagerOptions;
3235
import software.amazon.awssdk.crt.http.HttpClientConnectionManagerOptions;
@@ -37,6 +40,7 @@
3740
import software.amazon.awssdk.crt.http.HttpVersion;
3841
import software.amazon.awssdk.crt.io.ClientBootstrap;
3942
import software.amazon.awssdk.crt.io.SocketOptions;
43+
import software.amazon.awssdk.crt.io.TlsConnectionOptions;
4044
import software.amazon.awssdk.crt.io.TlsContext;
4145
import software.amazon.awssdk.crt.io.TlsContextOptions;
4246
import software.amazon.awssdk.http.Protocol;
@@ -54,6 +58,14 @@
5458
*/
5559
@SdkProtectedApi
5660
abstract class AwsCrtHttpClientBase implements SdkAutoCloseable {
61+
// TLS_NEGOTIATION_TIMEOUT diverges from the SDK global default (5s) for backwards compatibility:
62+
// the underlying CRT has always applied a 10s handshake timeout, so adopting the 5s global would silently tighten the
63+
// effective handshake timeout for existing CRT customers.
64+
static final AttributeMap AWS_CRT_HTTP_DEFAULTS =
65+
AttributeMap.builder()
66+
.put(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT, Duration.ofSeconds(10))
67+
.build();
68+
5769
private static final Logger log = Logger.loggerFor(AwsCrtHttpClientBase.class);
5870

5971
private static final String AWS_COMMON_RUNTIME = "AwsCommonRuntime";
@@ -72,6 +84,7 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable {
7284
private final int maxStreamsPerEndpoint;
7385
private final long connectionAcquisitionTimeout;
7486
private final TlsContextOptions tlsContextOptions;
87+
private final Duration tlsNegotiationTimeout;
7588
private boolean isClosed = false;
7689

7790
AwsCrtHttpClientBase(AwsCrtClientBuilderBase builder, AttributeMap config) {
@@ -93,6 +106,7 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable {
93106
this.bootstrap = registerOwnedResource(clientBootstrap);
94107
this.socketOptions = registerOwnedResource(clientSocketOptions);
95108
this.tlsContext = registerOwnedResource(clientTlsContext);
109+
this.tlsNegotiationTimeout = config.get(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT);
96110
this.readBufferSize = builder.getReadBufferSizeInBytes() == null ?
97111
DEFAULT_STREAM_WINDOW_SIZE : builder.getReadBufferSizeInBytes();
98112
this.maxStreamsPerEndpoint = config.get(SdkHttpConfigurationOption.MAX_CONNECTIONS);
@@ -122,18 +136,27 @@ String clientName() {
122136
return AWS_COMMON_RUNTIME;
123137
}
124138

139+
@SdkTestInternalApi
140+
Duration resolvedTlsNegotiationTimeout() {
141+
return tlsNegotiationTimeout;
142+
}
143+
125144
private HttpStreamManager createConnectionPool(URI uri) {
126145
log.debug(() ->
127146
String.format("Creating ConnectionPool for: URI:%s, MaxConns: %d, MaxStreams: %d",
128147
uri, maxStreamsPerEndpoint, maxStreamsPerEndpoint));
129148

130149
boolean isHttps = "https".equalsIgnoreCase(uri.getScheme());
131-
TlsContext poolTlsContext = isHttps ? tlsContext : null;
150+
TlsConnectionOptions poolTlsConnectionOptions = null;
151+
if (isHttps) {
152+
poolTlsConnectionOptions = registerOwnedResource(
153+
buildTlsConnectionOptions(tlsContext, tlsNegotiationTimeout, uri.getHost()));
154+
}
132155

133156
HttpClientConnectionManagerOptions h1Options = new HttpClientConnectionManagerOptions()
134157
.withClientBootstrap(bootstrap)
135158
.withSocketOptions(socketOptions)
136-
.withTlsContext(poolTlsContext)
159+
.withTlsConnectionOptions(poolTlsConnectionOptions)
137160
.withUri(uri)
138161
.withWindowSize(readBufferSize)
139162
.withMaxConnections(maxStreamsPerEndpoint)

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtClientBuilderBase.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ public BuilderT connectionAcquisitionTimeout(Duration connectionAcquisitionTimeo
106106
return thisBuilder();
107107
}
108108

109+
public BuilderT tlsNegotiationTimeout(Duration tlsNegotiationTimeout) {
110+
Validate.isPositive(tlsNegotiationTimeout, "tlsNegotiationTimeout");
111+
standardOptions.put(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT, tlsNegotiationTimeout);
112+
return thisBuilder();
113+
}
114+
115+
public void setTlsNegotiationTimeout(Duration tlsNegotiationTimeout) {
116+
tlsNegotiationTimeout(tlsNegotiationTimeout);
117+
}
118+
109119
public BuilderT tcpKeepAliveConfiguration(TcpKeepAliveConfiguration tcpKeepAliveConfiguration) {
110120
this.tcpKeepAliveConfiguration = tcpKeepAliveConfiguration;
111121
return thisBuilder();

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import software.amazon.awssdk.annotations.SdkInternalApi;
2121
import software.amazon.awssdk.crt.io.SocketOptions;
2222
import software.amazon.awssdk.crt.io.TlsCipherPreference;
23+
import software.amazon.awssdk.crt.io.TlsConnectionOptions;
24+
import software.amazon.awssdk.crt.io.TlsContext;
2325
import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration;
2426
import software.amazon.awssdk.utils.Logger;
2527
import software.amazon.awssdk.utils.NumericUtils;
@@ -53,6 +55,18 @@ public static SocketOptions buildSocketOptions(TcpKeepAliveConfiguration tcpKeep
5355
return clientSocketOptions;
5456
}
5557

58+
public static TlsConnectionOptions buildTlsConnectionOptions(TlsContext tlsContext, Duration tlsNegotiationTimeout,
59+
String serverName) {
60+
TlsConnectionOptions tlsConnectionOptions = new TlsConnectionOptions(tlsContext);
61+
if (tlsNegotiationTimeout != null) {
62+
tlsConnectionOptions.withTimeoutMs(NumericUtils.saturatedCast(tlsNegotiationTimeout.toMillis()));
63+
}
64+
if (serverName != null) {
65+
tlsConnectionOptions.withServerName(serverName);
66+
}
67+
return tlsConnectionOptions;
68+
}
69+
5670
public static TlsCipherPreference resolveCipherPreference(Boolean postQuantumTlsEnabled) {
5771
// As of v0.39.3, aws-crt-java prefers PQ by default, so only return the non-PQ-default policy
5872
// below if the caller explicitly disables PQ by passing in false.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.http.crt;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
21+
import java.time.Duration;
22+
import java.util.stream.Stream;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.Arguments;
26+
import org.junit.jupiter.params.provider.MethodSource;
27+
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
28+
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
29+
import software.amazon.awssdk.utils.AttributeMap;
30+
31+
class AwsCrtAsyncHttpClientTest extends AwsCrtHttpClientTestBase {
32+
33+
@ParameterizedTest(name = "{0}")
34+
@MethodSource("invalidTlsNegotiationTimeouts")
35+
void tlsNegotiationTimeout_invalidDuration_shouldThrowException(String description, Duration input,
36+
String expectedMessageFragment) {
37+
assertThatThrownBy(() -> AwsCrtAsyncHttpClient.builder().tlsNegotiationTimeout(input).build())
38+
.isInstanceOf(IllegalArgumentException.class)
39+
.hasMessageContaining(expectedMessageFragment);
40+
}
41+
42+
@ParameterizedTest(name = "[async] {0}")
43+
@MethodSource("resolutionMatrix")
44+
void asyncBuilder_resolvedTlsNegotiationTimeout_matchesPathBPrecedence(String description, Duration customer,
45+
Duration serviceDefault, Duration expected) {
46+
AwsCrtAsyncHttpClient.Builder builder = AwsCrtAsyncHttpClient.builder();
47+
if (customer != null) {
48+
builder.tlsNegotiationTimeout(customer);
49+
}
50+
51+
try (SdkAsyncHttpClient client = buildAsync(builder, serviceDefault)) {
52+
assertThat(((AwsCrtAsyncHttpClient) client).resolvedTlsNegotiationTimeout()).isEqualTo(expected);
53+
}
54+
}
55+
56+
@Test
57+
void asyncBuilder_buildWithDefaults_serviceDefaultsLacksTlsNegotiationTimeout_resolvesToCrtDefault10s() {
58+
try (SdkAsyncHttpClient client = AwsCrtAsyncHttpClient.builder().buildWithDefaults(AttributeMap.empty())) {
59+
assertThat(((AwsCrtAsyncHttpClient) client).resolvedTlsNegotiationTimeout()).isEqualTo(CRT_DEFAULT);
60+
}
61+
}
62+
63+
private static SdkAsyncHttpClient buildAsync(AwsCrtAsyncHttpClient.Builder builder, Duration serviceDefault) {
64+
return serviceDefault == null
65+
? builder.build()
66+
: builder.buildWithDefaults(serviceDefaultsMap(serviceDefault));
67+
}
68+
69+
}

http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientWireMockTest.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import com.github.tomakehurst.wiremock.junit.WireMockRule;
3131
import java.net.URI;
32+
import java.time.Duration;
3233
import java.util.concurrent.TimeUnit;
3334
import org.junit.AfterClass;
3435
import org.junit.BeforeClass;
@@ -39,6 +40,7 @@
3940
import software.amazon.awssdk.http.HttpMetric;
4041
import software.amazon.awssdk.http.Protocol;
4142
import software.amazon.awssdk.http.RecordingResponseHandler;
43+
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
4244
import software.amazon.awssdk.http.SdkHttpRequest;
4345
import software.amazon.awssdk.http.async.AsyncExecuteRequest;
4446
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
@@ -48,7 +50,8 @@
4850
public class AwsCrtAsyncHttpClientWireMockTest {
4951
@Rule
5052
public WireMockRule mockServer = new WireMockRule(wireMockConfig()
51-
.dynamicPort());
53+
.dynamicPort()
54+
.dynamicHttpsPort());
5255

5356
@BeforeClass
5457
public static void setup() {
@@ -90,6 +93,31 @@ public void sharedEventLoopGroup_closeOneClient_shouldNotAffectOtherClients() th
9093
}
9194
}
9295

96+
@Test
97+
public void tlsNegotiationTimeout_customValue_clientStartsSuccessfully() throws Exception {
98+
AttributeMap defaults = AttributeMap.builder().put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true).build();
99+
try (SdkAsyncHttpClient client = AwsCrtAsyncHttpClient.builder()
100+
.tlsNegotiationTimeout(Duration.ofSeconds(3))
101+
.buildWithDefaults(defaults)) {
102+
makeSimpleHttpsRequest(client);
103+
}
104+
}
105+
106+
private RecordingResponseHandler makeSimpleHttpsRequest(SdkAsyncHttpClient client) throws Exception {
107+
String body = randomAlphabetic(10);
108+
URI uri = URI.create("https://localhost:" + mockServer.httpsPort());
109+
stubFor(any(urlPathEqualTo("/")).willReturn(aResponse().withBody(body)));
110+
SdkHttpRequest request = createRequest(uri);
111+
RecordingResponseHandler recorder = new RecordingResponseHandler();
112+
client.execute(AsyncExecuteRequest.builder()
113+
.request(request)
114+
.requestContentPublisher(createProvider(""))
115+
.responseHandler(recorder)
116+
.build());
117+
recorder.completeFuture().get(5, TimeUnit.SECONDS);
118+
return recorder;
119+
}
120+
93121
/**
94122
* Make a simple async request and wait for it to finish.
95123
*

0 commit comments

Comments
 (0)