Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "AWS Common Runtime HTTP Client",
"contributor": "",
"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."
}
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,17 @@ AwsCrtAsyncHttpClient.Builder connectionHealthConfiguration(Consumer<ConnectionH
*/
AwsCrtAsyncHttpClient.Builder connectionAcquisitionTimeout(Duration connectionAcquisitionTimeout);

/**
* Configure the maximum amount of time that a TLS handshake is allowed to take from the time the CLIENT HELLO
* message is sent to the time the client and server have fully negotiated ciphers and exchanged keys.
*
* <p>By default, it's 10 seconds.
*
* @param tlsNegotiationTimeout the timeout duration; must be positive
* @return this builder for method chaining.
*/
AwsCrtAsyncHttpClient.Builder tlsNegotiationTimeout(Duration tlsNegotiationTimeout);

/**
* Configure whether to enable {@code tcpKeepAlive} and relevant configuration for all connections established by this
* client.
Expand Down Expand Up @@ -269,13 +280,15 @@ public Builder protocol(Protocol protocol) {
@Override
public SdkAsyncHttpClient build() {
return new AwsCrtAsyncHttpClient(this, getAttributeMap().build()
.merge(AwsCrtHttpClientBase.AWS_CRT_HTTP_DEFAULTS)
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
}

@Override
public SdkAsyncHttpClient buildWithDefaults(AttributeMap serviceDefaults) {
return new AwsCrtAsyncHttpClient(this, getAttributeMap().build()
.merge(serviceDefaults)
.merge(AwsCrtHttpClientBase.AWS_CRT_HTTP_DEFAULTS)
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,17 @@ AwsCrtHttpClient.Builder connectionHealthConfiguration(Consumer<ConnectionHealth
*/
AwsCrtHttpClient.Builder connectionAcquisitionTimeout(Duration connectionAcquisitionTimeout);

/**
* Configure the maximum amount of time that a TLS handshake is allowed to take from the time the CLIENT HELLO
* message is sent to the time the client and server have fully negotiated ciphers and exchanged keys.
*
* <p>By default, it's 10 seconds.
*
* @param tlsNegotiationTimeout the timeout duration; must be positive
* @return this builder for method chaining.
*/
AwsCrtHttpClient.Builder tlsNegotiationTimeout(Duration tlsNegotiationTimeout);

/**
* Configure whether to enable {@code tcpKeepAlive} and relevant configuration for all connections established by this
* client.
Expand Down Expand Up @@ -307,13 +318,15 @@ private static final class DefaultBuilder
@Override
public AwsCrtHttpClient build() {
return new AwsCrtHttpClient(this, getAttributeMap().build()
.merge(AwsCrtHttpClientBase.AWS_CRT_HTTP_DEFAULTS)
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
}

@Override
public AwsCrtHttpClient buildWithDefaults(AttributeMap serviceDefaults) {
return new AwsCrtHttpClient(this, getAttributeMap().build()
.merge(serviceDefaults)
.merge(AwsCrtHttpClientBase.AWS_CRT_HTTP_DEFAULTS)
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@
import static software.amazon.awssdk.crtcore.CrtConfigurationUtils.resolveProxy;
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.PROTOCOL;
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildSocketOptions;
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildTlsConnectionOptions;
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveCipherPreference;
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;

import java.net.URI;
import java.time.Duration;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
import software.amazon.awssdk.crt.CrtResource;
import software.amazon.awssdk.crt.http.Http2StreamManagerOptions;
import software.amazon.awssdk.crt.http.HttpClientConnectionManagerOptions;
Expand All @@ -37,6 +40,7 @@
import software.amazon.awssdk.crt.http.HttpVersion;
import software.amazon.awssdk.crt.io.ClientBootstrap;
import software.amazon.awssdk.crt.io.SocketOptions;
import software.amazon.awssdk.crt.io.TlsConnectionOptions;
import software.amazon.awssdk.crt.io.TlsContext;
import software.amazon.awssdk.crt.io.TlsContextOptions;
import software.amazon.awssdk.http.Protocol;
Expand All @@ -54,6 +58,14 @@
*/
@SdkProtectedApi
abstract class AwsCrtHttpClientBase implements SdkAutoCloseable {
// TLS_NEGOTIATION_TIMEOUT diverges from the SDK global default (5s) for backwards compatibility:
// the underlying CRT has always applied a 10s handshake timeout, so adopting the 5s global would silently tighten the
// effective handshake timeout for existing CRT customers.
static final AttributeMap AWS_CRT_HTTP_DEFAULTS =
AttributeMap.builder()
.put(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT, Duration.ofSeconds(10))
.build();

private static final Logger log = Logger.loggerFor(AwsCrtHttpClientBase.class);

private static final String AWS_COMMON_RUNTIME = "AwsCommonRuntime";
Expand All @@ -66,12 +78,14 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable {
private final ClientBootstrap bootstrap;
private final SocketOptions socketOptions;
private final TlsContext tlsContext;
private final TlsConnectionOptions tlsConnectionOptions;
private final HttpProxyOptions proxyOptions;
private final HttpMonitoringOptions monitoringOptions;
private final long maxConnectionIdleInMilliseconds;
private final int maxStreamsPerEndpoint;
private final long connectionAcquisitionTimeout;
private final TlsContextOptions tlsContextOptions;
private final Duration tlsNegotiationTimeout;
private boolean isClosed = false;

AwsCrtHttpClientBase(AwsCrtClientBuilderBase builder, AttributeMap config) {
Expand All @@ -93,6 +107,9 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable {
this.bootstrap = registerOwnedResource(clientBootstrap);
this.socketOptions = registerOwnedResource(clientSocketOptions);
this.tlsContext = registerOwnedResource(clientTlsContext);
this.tlsNegotiationTimeout = config.get(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT);
this.tlsConnectionOptions = registerOwnedResource(
buildTlsConnectionOptions(clientTlsContext, tlsNegotiationTimeout));
this.readBufferSize = builder.getReadBufferSizeInBytes() == null ?
DEFAULT_STREAM_WINDOW_SIZE : builder.getReadBufferSizeInBytes();
this.maxStreamsPerEndpoint = config.get(SdkHttpConfigurationOption.MAX_CONNECTIONS);
Expand Down Expand Up @@ -122,18 +139,23 @@ String clientName() {
return AWS_COMMON_RUNTIME;
}

@SdkTestInternalApi
Duration resolvedTlsNegotiationTimeout() {
return tlsNegotiationTimeout;
}

private HttpStreamManager createConnectionPool(URI uri) {
log.debug(() ->
String.format("Creating ConnectionPool for: URI:%s, MaxConns: %d, MaxStreams: %d",
uri, maxStreamsPerEndpoint, maxStreamsPerEndpoint));

boolean isHttps = "https".equalsIgnoreCase(uri.getScheme());
TlsContext poolTlsContext = isHttps ? tlsContext : null;
TlsConnectionOptions poolTlsConnectionOptions = isHttps ? tlsConnectionOptions : null;

HttpClientConnectionManagerOptions h1Options = new HttpClientConnectionManagerOptions()
.withClientBootstrap(bootstrap)
.withSocketOptions(socketOptions)
.withTlsContext(poolTlsContext)
.withTlsConnectionOptions(poolTlsConnectionOptions)
.withUri(uri)
.withWindowSize(readBufferSize)
.withMaxConnections(maxStreamsPerEndpoint)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ public BuilderT connectionAcquisitionTimeout(Duration connectionAcquisitionTimeo
return thisBuilder();
}

public BuilderT tlsNegotiationTimeout(Duration tlsNegotiationTimeout) {
Validate.isPositive(tlsNegotiationTimeout, "tlsNegotiationTimeout");
standardOptions.put(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT, tlsNegotiationTimeout);
return thisBuilder();
}

public void setTlsNegotiationTimeout(Duration tlsNegotiationTimeout) {
tlsNegotiationTimeout(tlsNegotiationTimeout);
}

public BuilderT tcpKeepAliveConfiguration(TcpKeepAliveConfiguration tcpKeepAliveConfiguration) {
this.tcpKeepAliveConfiguration = tcpKeepAliveConfiguration;
return thisBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.crt.io.SocketOptions;
import software.amazon.awssdk.crt.io.TlsCipherPreference;
import software.amazon.awssdk.crt.io.TlsConnectionOptions;
import software.amazon.awssdk.crt.io.TlsContext;
import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration;
import software.amazon.awssdk.utils.Logger;
import software.amazon.awssdk.utils.NumericUtils;
Expand Down Expand Up @@ -53,6 +55,14 @@ public static SocketOptions buildSocketOptions(TcpKeepAliveConfiguration tcpKeep
return clientSocketOptions;
}

public static TlsConnectionOptions buildTlsConnectionOptions(TlsContext tlsContext, Duration tlsNegotiationTimeout) {
TlsConnectionOptions tlsConnectionOptions = new TlsConnectionOptions(tlsContext);
if (tlsNegotiationTimeout != null) {
tlsConnectionOptions.withTimeoutMs(NumericUtils.saturatedCast(tlsNegotiationTimeout.toMillis()));
}
return tlsConnectionOptions;
}

public static TlsCipherPreference resolveCipherPreference(Boolean postQuantumTlsEnabled) {
// As of v0.39.3, aws-crt-java prefers PQ by default, so only return the non-PQ-default policy
// below if the caller explicitly disables PQ by passing in false.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.http.crt;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.time.Duration;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
import software.amazon.awssdk.utils.AttributeMap;

class AwsCrtAsyncHttpClientTest extends AwsCrtHttpClientTestBase {

@ParameterizedTest(name = "{0}")
@MethodSource("invalidTlsNegotiationTimeouts")
void tlsNegotiationTimeout_invalidDuration_shouldThrowException(String description, Duration input,
String expectedMessageFragment) {
assertThatThrownBy(() -> AwsCrtAsyncHttpClient.builder().tlsNegotiationTimeout(input).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining(expectedMessageFragment);
}

@ParameterizedTest(name = "[async] {0}")
@MethodSource("resolutionMatrix")
void asyncBuilder_resolvedTlsNegotiationTimeout_matchesPathBPrecedence(String description, Duration customer,
Duration serviceDefault, Duration expected) {
AwsCrtAsyncHttpClient.Builder builder = AwsCrtAsyncHttpClient.builder();
if (customer != null) {
builder.tlsNegotiationTimeout(customer);
}

try (SdkAsyncHttpClient client = buildAsync(builder, serviceDefault)) {
assertThat(((AwsCrtAsyncHttpClient) client).resolvedTlsNegotiationTimeout()).isEqualTo(expected);
}
}

@Test
void asyncBuilder_buildWithDefaults_serviceDefaultsLacksTlsNegotiationTimeout_resolvesToCrtDefault10s() {
try (SdkAsyncHttpClient client = AwsCrtAsyncHttpClient.builder().buildWithDefaults(AttributeMap.empty())) {
assertThat(((AwsCrtAsyncHttpClient) client).resolvedTlsNegotiationTimeout()).isEqualTo(CRT_DEFAULT);
}
}

private static SdkAsyncHttpClient buildAsync(AwsCrtAsyncHttpClient.Builder builder, Duration serviceDefault) {
return serviceDefault == null
? builder.build()
: builder.buildWithDefaults(serviceDefaultsMap(serviceDefault));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import com.github.tomakehurst.wiremock.junit.WireMockRule;
import java.net.URI;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import org.junit.AfterClass;
import org.junit.BeforeClass;
Expand All @@ -39,6 +40,7 @@
import software.amazon.awssdk.http.HttpMetric;
import software.amazon.awssdk.http.Protocol;
import software.amazon.awssdk.http.RecordingResponseHandler;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.http.async.AsyncExecuteRequest;
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
Expand All @@ -48,7 +50,8 @@
public class AwsCrtAsyncHttpClientWireMockTest {
@Rule
public WireMockRule mockServer = new WireMockRule(wireMockConfig()
.dynamicPort());
.dynamicPort()
.dynamicHttpsPort());

@BeforeClass
public static void setup() {
Expand Down Expand Up @@ -90,6 +93,31 @@ public void sharedEventLoopGroup_closeOneClient_shouldNotAffectOtherClients() th
}
}

@Test
public void tlsNegotiationTimeout_customValue_clientStartsSuccessfully() throws Exception {
AttributeMap defaults = AttributeMap.builder().put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true).build();
try (SdkAsyncHttpClient client = AwsCrtAsyncHttpClient.builder()
.tlsNegotiationTimeout(Duration.ofSeconds(3))
.buildWithDefaults(defaults)) {
makeSimpleHttpsRequest(client);
}
}

private RecordingResponseHandler makeSimpleHttpsRequest(SdkAsyncHttpClient client) throws Exception {
String body = randomAlphabetic(10);
URI uri = URI.create("https://localhost:" + mockServer.httpsPort());
stubFor(any(urlPathEqualTo("/")).willReturn(aResponse().withBody(body)));
SdkHttpRequest request = createRequest(uri);
RecordingResponseHandler recorder = new RecordingResponseHandler();
client.execute(AsyncExecuteRequest.builder()
.request(request)
.requestContentPublisher(createProvider(""))
.responseHandler(recorder)
.build());
recorder.completeFuture().get(5, TimeUnit.SECONDS);
return recorder;
}

/**
* Make a simple async request and wait for it to finish.
*
Expand Down
Loading
Loading