Skip to content

Expose tlsNegotiationTimeout for netty client #2806

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 1, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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 @@
{
"category": "Netty NIO HTTP Client",
"contributor": "",
"type": "feature",
"description": "Allow users to configure tlsNegotiationTimeout on NettyNioAsyncHttpClient"
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,20 @@ public final class SdkHttpConfigurationOption<T> extends AttributeMap.Key<T> {
public static final SdkHttpConfigurationOption<TlsTrustManagersProvider> TLS_TRUST_MANAGERS_PROVIDER =
new SdkHttpConfigurationOption<>("TlsTrustManagersProvider", TlsTrustManagersProvider.class);

/**
* An upper limit on how long from the time 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.
*/
public static final SdkHttpConfigurationOption<Duration> TLS_NEGOTIATION_TIMEOUT =
new SdkHttpConfigurationOption<>("TlsNegotiationTimeout", Duration.class);

private static final Duration DEFAULT_SOCKET_READ_TIMEOUT = Duration.ofSeconds(30);
private static final Duration DEFAULT_SOCKET_WRITE_TIMEOUT = Duration.ofSeconds(30);
private static final Duration DEFAULT_CONNECTION_TIMEOUT = Duration.ofSeconds(2);
private static final Duration DEFAULT_CONNECTION_ACQUIRE_TIMEOUT = Duration.ofSeconds(10);
private static final Duration DEFAULT_CONNECTION_MAX_IDLE_TIMEOUT = Duration.ofSeconds(60);
private static final Duration DEFAULT_CONNECTION_TIME_TO_LIVE = Duration.ZERO;
private static final Duration DEFAULT_TLS_HANDSHAKE_TIMEOUT = Duration.ofSeconds(10);
private static final Boolean DEFAULT_REAP_IDLE_CONNECTIONS = Boolean.TRUE;
private static final int DEFAULT_MAX_CONNECTIONS = 50;
private static final int DEFAULT_MAX_CONNECTION_ACQUIRES = 10_000;
Expand Down Expand Up @@ -150,6 +158,7 @@ public final class SdkHttpConfigurationOption<T> extends AttributeMap.Key<T> {
.put(TCP_KEEPALIVE, DEFAULT_TCP_KEEPALIVE)
.put(TLS_KEY_MANAGERS_PROVIDER, DEFAULT_TLS_KEY_MANAGERS_PROVIDER)
.put(TLS_TRUST_MANAGERS_PROVIDER, DEFAULT_TLS_TRUST_MANAGERS_PROVIDER)
.put(TLS_NEGOTIATION_TIMEOUT, DEFAULT_TLS_HANDSHAKE_TIMEOUT)
.build();

private final String name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,14 @@ public interface Builder extends SdkAsyncHttpClient.Builder<NettyNioAsyncHttpCli
*/
Builder connectionMaxIdleTime(Duration maxIdleConnectionTimeout);

/**
* 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.
* @param tlsNegotiationTimeout the timeout duration
* @return this builder for method chaining.
*/
Builder tlsNegotiationTimeout(Duration tlsNegotiationTimeout);

/**
* Configure whether the idle connections in the connection pool should be closed.
* <p>
Expand Down Expand Up @@ -578,6 +586,17 @@ public void setUseIdleConnectionReaper(Boolean useIdleConnectionReaper) {
useIdleConnectionReaper(useIdleConnectionReaper);
}

@Override
public Builder tlsNegotiationTimeout(Duration tlsNegotiationTimeout) {
Validate.isPositive(tlsNegotiationTimeout, "tlsNegotiationTimeout");
standardOptions.put(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT, tlsNegotiationTimeout);
return this;
}

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

@Override
public Builder eventLoopGroup(SdkEventLoopGroup eventLoopGroup) {
this.eventLoopGroup = eventLoopGroup;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ protected SimpleChannelPoolAwareChannelPool newPool(URI key) {
tcpChannelPool = new BetterSimpleChannelPool(bootstrap, NOOP_HANDLER);
baseChannelPool = new Http1TunnelConnectionPool(bootstrap.config().group().next(), tcpChannelPool, sslContext,
proxyAddress(key), proxyConfiguration.username(), proxyConfiguration.password(),
key, pipelineInitializer);
key, pipelineInitializer, configuration);
} else {
tcpChannelPool = new BetterSimpleChannelPool(bootstrap, pipelineInitializer);
baseChannelPool = tcpChannelPool;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ public void channelCreated(Channel ch) {
ChannelPipeline pipeline = ch.pipeline();
if (sslCtx != null) {

SslHandler sslHandler = newSslHandler(sslCtx, ch.alloc(), poolKey.getHost(), poolKey.getPort());
SslHandler sslHandler = newSslHandler(sslCtx, ch.alloc(), poolKey.getHost(), poolKey.getPort(),
configuration.tlsHandshakeTimeout());

pipeline.addLast(sslHandler);
pipeline.addLast(SslCloseCompletionEventHandler.getInstance());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,30 @@ public class Http1TunnelConnectionPool implements ChannelPool {
private final URI remoteAddress;
private final ChannelPoolHandler handler;
private final InitHandlerSupplier initHandlerSupplier;
private final NettyConfiguration nettyConfiguration;

public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext,
URI proxyAddress, String proxyUsername, String proxyPassword,
URI remoteAddress, ChannelPoolHandler handler) {
URI remoteAddress, ChannelPoolHandler handler, NettyConfiguration nettyConfiguration) {
this(eventLoop, delegate, sslContext,
proxyAddress, proxyUsername, proxyPassword, remoteAddress, handler,
ProxyTunnelInitHandler::new);
proxyAddress, proxyUsername, proxyPassword, remoteAddress, handler,
ProxyTunnelInitHandler::new, nettyConfiguration);
}

public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext,
URI proxyAddress, URI remoteAddress, ChannelPoolHandler handler) {
URI proxyAddress, URI remoteAddress, ChannelPoolHandler handler,
NettyConfiguration nettyConfiguration) {
this(eventLoop, delegate, sslContext,
proxyAddress, null, null, remoteAddress, handler,
ProxyTunnelInitHandler::new);
proxyAddress, null, null, remoteAddress, handler,
ProxyTunnelInitHandler::new, nettyConfiguration);

}

@SdkTestInternalApi
Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext,
URI proxyAddress, String proxyUser, String proxyPassword, URI remoteAddress,
ChannelPoolHandler handler, InitHandlerSupplier initHandlerSupplier) {
ChannelPoolHandler handler, InitHandlerSupplier initHandlerSupplier,
NettyConfiguration nettyConfiguration) {
this.eventLoop = eventLoop;
this.delegate = delegate;
this.sslContext = sslContext;
Expand All @@ -84,6 +87,7 @@ public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslC
this.remoteAddress = remoteAddress;
this.handler = handler;
this.initHandlerSupplier = initHandlerSupplier;
this.nettyConfiguration = nettyConfiguration;
}

@Override
Expand Down Expand Up @@ -164,7 +168,8 @@ private SslHandler createSslHandlerIfNeeded(ByteBufAllocator alloc) {
return null;
}

return newSslHandler(sslContext, alloc, proxyAddress.getHost(), proxyAddress.getPort());
return newSslHandler(sslContext, alloc, proxyAddress.getHost(), proxyAddress.getPort(),
nettyConfiguration.tlsHandshakeTimeout());
}

private static boolean isTunnelEstablished(Channel ch) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES;
import static software.amazon.awssdk.utils.NumericUtils.saturatedCast;

import java.time.Duration;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.http.TlsKeyManagersProvider;
Expand Down Expand Up @@ -102,4 +103,8 @@ public boolean trustAllCertificates() {
public boolean tcpKeepAlive() {
return configuration.get(TCP_KEEPALIVE);
}

public Duration tlsHandshakeTimeout() {
return configuration.get(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.concurrent.Promise;
import io.netty.util.concurrent.SucceededFuture;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Function;
import javax.net.ssl.SSLEngine;
Expand Down Expand Up @@ -196,10 +198,12 @@ public static <T> AttributeKey<T> getOrCreateAttributeKey(String attr) {
/**
* @return a new {@link SslHandler} with ssl engine configured
*/
public static SslHandler newSslHandler(SslContext sslContext, ByteBufAllocator alloc, String peerHost, int peerPort) {
public static SslHandler newSslHandler(SslContext sslContext, ByteBufAllocator alloc, String peerHost, int peerPort,
Duration handshakeTimeout) {
// Need to provide host and port to enable SNI
// https://github.com/netty/netty/issues/3801#issuecomment-104274440
SslHandler sslHandler = sslContext.newHandler(alloc, peerHost, peerPort);
sslHandler.setHandshakeTimeout(handshakeTimeout.toMillis(), TimeUnit.MILLISECONDS);
configureSslEngine(sslHandler.engine());
return sslHandler;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,21 @@ public void createNettyClient_ReadWriteTimeoutCanBeZero() throws Exception {
customClient.close();
}

@Test
public void createNettyClient_tlsNegotiationTimeoutNotPositive_shouldThrowException() throws Exception {
assertThatThrownBy(() -> NettyNioAsyncHttpClient.builder()
.tlsNegotiationTimeout(Duration.ZERO)
.build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("must be positive");
assertThatThrownBy(() -> NettyNioAsyncHttpClient.builder()
.tlsNegotiationTimeout(Duration.ofSeconds(-1))
.build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("must be positive");
}


@Test
public void metricsAreCollectedWhenMaxPendingConnectionAcquisitionsAreExceeded() throws Exception {
SdkAsyncHttpClient customClient = NettyNioAsyncHttpClient.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static software.amazon.awssdk.http.nio.netty.internal.Http1TunnelConnectionPool.TUNNEL_ESTABLISHED_KEY;

import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
Expand All @@ -43,7 +44,6 @@
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
Expand All @@ -55,7 +55,7 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.testng.collections.Maps;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;

/**
* Unit tests for {@link Http1TunnelConnectionPool}.
Expand Down Expand Up @@ -95,6 +95,8 @@ public class Http1TunnelConnectionPoolTest {
@Mock
public ChannelId mockId;

public NettyConfiguration configuration;

@Before
public void methodSetup() {
Future<Channel> channelFuture = GROUP.next().newSucceededFuture(mockChannel);
Expand All @@ -106,6 +108,7 @@ public void methodSetup() {
when(mockChannel.attr(eq(TUNNEL_ESTABLISHED_KEY))).thenReturn(mockAttr);
when(mockChannel.id()).thenReturn(mockId);
when(mockChannel.pipeline()).thenReturn(mockPipeline);
configuration = new NettyConfiguration(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS);
}

@AfterClass
Expand All @@ -116,7 +119,7 @@ public static void teardown() {
@Test
public void tunnelAlreadyEstablished_doesNotAddInitHandler() {
Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null,
HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler);
HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration);

when(mockAttr.get()).thenReturn(true);

Expand All @@ -128,7 +131,7 @@ public void tunnelAlreadyEstablished_doesNotAddInitHandler() {
@Test(timeout = 1000)
public void tunnelNotEstablished_addsInitHandler() throws InterruptedException {
Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null,
HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler);
HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration);

when(mockAttr.get()).thenReturn(false);

Expand All @@ -150,7 +153,7 @@ public void tunnelInitFails_acquireFutureFails() {
};

Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null,
HTTP_PROXY_ADDRESS,null, null, REMOTE_ADDRESS, mockHandler, supplier);
HTTP_PROXY_ADDRESS,null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration);

Future<Channel> acquireFuture = tunnelPool.acquire();

Expand All @@ -165,7 +168,7 @@ public void tunnelInitSucceeds_acquireFutureSucceeds() {
};

Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null,
HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier);
HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration);

Future<Channel> acquireFuture = tunnelPool.acquire();

Expand All @@ -175,7 +178,7 @@ public void tunnelInitSucceeds_acquireFutureSucceeds() {
@Test
public void acquireFromDelegatePoolFails_failsFuture() {
Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null,
HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler);
HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration);

when(delegatePool.acquire(any(Promise.class))).thenReturn(GROUP.next().newFailedFuture(new IOException("boom")));

Expand All @@ -198,7 +201,7 @@ public void sslContextProvided_andProxyUsingHttps_addsSslHandler() {
};

Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, mockSslCtx,
HTTPS_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier);
HTTPS_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration);

tunnelPool.acquire().awaitUninterruptibly();

Expand All @@ -219,7 +222,7 @@ public void sslContextProvided_andProxyNotUsingHttps_doesNotAddSslHandler() {
};

Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, mockSslCtx,
HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier);
HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration);

tunnelPool.acquire().awaitUninterruptibly();

Expand All @@ -232,15 +235,15 @@ public void sslContextProvided_andProxyNotUsingHttps_doesNotAddSslHandler() {
@Test
public void release_releasedToDelegatePool() {
Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null,
HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler);
HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration);
tunnelPool.release(mockChannel);
verify(delegatePool).release(eq(mockChannel), any(Promise.class));
}

@Test
public void release_withGivenPromise_releasedToDelegatePool() {
Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null,
HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler);
HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration);
Promise mockPromise = mock(Promise.class);
tunnelPool.release(mockChannel, mockPromise);
verify(delegatePool).release(eq(mockChannel), eq(mockPromise));
Expand All @@ -249,7 +252,7 @@ public void release_withGivenPromise_releasedToDelegatePool() {
@Test
public void close_closesDelegatePool() {
Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null,
HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler);
HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration);
tunnelPool.close();
verify(delegatePool).close();
}
Expand All @@ -266,7 +269,7 @@ public void proxyAuthProvided_addInitHandler_withAuth(){
};

Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null,
HTTP_PROXY_ADDRESS, PROXY_USER, PROXY_PASSWORD, REMOTE_ADDRESS, mockHandler, supplier);
HTTP_PROXY_ADDRESS, PROXY_USER, PROXY_PASSWORD, REMOTE_ADDRESS, mockHandler, supplier, configuration);

tunnelPool.acquire().awaitUninterruptibly();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.netty.handler.ssl.SslHandler;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.EventExecutor;
import java.time.Duration;
import javax.net.ssl.SSLEngine;
import org.junit.Test;
import software.amazon.awssdk.http.nio.netty.internal.MockChannel;
Expand All @@ -46,7 +47,10 @@ public void newSslHandler_sslEngineShouldBeConfigured() throws Exception {
Channel channel = null;
try {
channel = new MockChannel();
SslHandler sslHandler = NettyUtils.newSslHandler(sslContext, channel.alloc(), "localhost", 80);
SslHandler sslHandler = NettyUtils.newSslHandler(sslContext, channel.alloc(), "localhost", 80,
Duration.ofMillis(1000));

assertThat(sslHandler.getHandshakeTimeoutMillis()).isEqualTo(1000);
SSLEngine engine = sslHandler.engine();
assertThat(engine.getSSLParameters().getEndpointIdentificationAlgorithm()).isEqualTo("HTTPS");
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ private HttpURLConnection createDefaultConnection(URI uri, SSLSocketFactory sock
if (options.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES)) {
httpsConnection.setHostnameVerifier(NoOpHostNameVerifier.INSTANCE);
}

httpsConnection.setSSLSocketFactory(socketFactory);
}

Expand Down