Skip to content
Merged
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,104 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.testing.async;

import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.TlsConfig;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.testing.SSLTestContexts;
import org.apache.hc.client5.testing.tls.TlsHandshakeTimeoutServer;
import org.apache.hc.core5.reactor.IOReactorConfig;
import org.apache.hc.core5.util.TimeValue;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ExecutionException;

import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class TestAsyncTlsHandshakeTimeout {
private static final Duration EXPECTED_TIMEOUT = Duration.ofMillis(500);

@Timeout(5)
@ParameterizedTest
@ValueSource(strings = { "false", "true" })
void testTimeout(final boolean sendServerHello) throws Exception {
final PoolingAsyncClientConnectionManager connMgr = PoolingAsyncClientConnectionManagerBuilder.create()
.setDefaultConnectionConfig(ConnectionConfig.custom()
.setConnectTimeout(5, SECONDS)
.setSocketTimeout(5, SECONDS)
.build())
.setTlsStrategy(new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
.setDefaultTlsConfig(TlsConfig.custom()
.setHandshakeTimeout(EXPECTED_TIMEOUT.toMillis(), MILLISECONDS)
.build())
.build();
try (
final TlsHandshakeTimeoutServer server = new TlsHandshakeTimeoutServer(sendServerHello);
final CloseableHttpAsyncClient client = HttpAsyncClientBuilder.create()
.setIOReactorConfig(IOReactorConfig.custom()
.setSelectInterval(TimeValue.ofMilliseconds(50))
.build())
.setConnectionManager(connMgr)
.build()
) {
server.start();
client.start();

final SimpleHttpRequest request = SimpleHttpRequest.create("GET", "https://127.0.0.1:" + server.getPort());
assertTimeout(request, client);
}
}

private static void assertTimeout(final SimpleHttpRequest request, final CloseableHttpAsyncClient client) {
final long startTime = System.nanoTime();
final Throwable ex = assertThrows(ExecutionException.class,
() -> client.execute(request, null).get()).getCause();
final Duration actualTime = Duration.of(System.nanoTime() - startTime, ChronoUnit.NANOS);
assertTrue(actualTime.toMillis() > EXPECTED_TIMEOUT.toMillis() / 2,
format("Handshake attempt timed out too soon (only %,d out of %,d ms)",
actualTime.toMillis(),
EXPECTED_TIMEOUT.toMillis()));
assertTrue(actualTime.toMillis() < EXPECTED_TIMEOUT.toMillis() * 2,
format("Handshake attempt timed out too late (%,d out of %,d ms)",
actualTime.toMillis(),
EXPECTED_TIMEOUT.toMillis()));
assertTrue(ex.getMessage().contains(EXPECTED_TIMEOUT.toMillis() + " MILLISECONDS"), ex.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.testing.sync;

import org.apache.hc.client5.http.ConnectTimeoutException;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.TlsConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.testing.SSLTestContexts;
import org.apache.hc.client5.testing.tls.TlsHandshakeTimeoutServer;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import javax.net.ssl.SSLException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;

import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.hc.core5.util.ReflectionUtils.determineJRELevel;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeFalse;

public class TestTlsHandshakeTimeout {
private static final Duration EXPECTED_TIMEOUT = Duration.ofMillis(500);

@Timeout(5)
@ParameterizedTest
@ValueSource(strings = { "false", "true" })
void testTimeout(final boolean sendServerHello) throws Exception {
final PoolingHttpClientConnectionManager connMgr = PoolingHttpClientConnectionManagerBuilder.create()
.setDefaultConnectionConfig(ConnectionConfig.custom()
.setConnectTimeout(5, SECONDS)
.setSocketTimeout(5, SECONDS)
.build())
.setTlsSocketStrategy(new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
.setDefaultTlsConfig(TlsConfig.custom()
.setHandshakeTimeout(EXPECTED_TIMEOUT.toMillis(), MILLISECONDS)
.build())
.build();
try (
final TlsHandshakeTimeoutServer server = new TlsHandshakeTimeoutServer(sendServerHello);
final CloseableHttpClient client = HttpClientBuilder.create()
.setConnectionManager(connMgr)
.build()
) {
server.start();

final HttpUriRequestBase request = new HttpGet("https://127.0.0.1:" + server.getPort());
assertTimeout(request, client);
}
}

@SuppressWarnings("deprecation")
private static void assertTimeout(final ClassicHttpRequest request, final HttpClient client) {
final long startTime = System.nanoTime();
final Exception ex = assertThrows(Exception.class, () -> client.execute(request));
final Duration actualTime = Duration.of(System.nanoTime() - startTime, ChronoUnit.NANOS);

if (determineJRELevel() == 8) {
assertInstanceOf(SSLException.class, ex);
} else {
assertInstanceOf(ConnectTimeoutException.class, ex);
}
assertTrue(ex.getMessage().contains("Read timed out"), ex.getMessage());

// There is a bug in Java 11: after the handshake times out, the SSLSocket implementation performs a blocking
// read on the socket to wait for close_notify or alert. This operation blocks until the read times out,
// which means that TLS handshakes take twice as long to time out on Java 11. Without a workaround, the only
// option is to skip the timeout duration assertions on Java 11.
assumeFalse(determineJRELevel() == 11, "TLS handshake timeouts are buggy on Java 11");

assertTrue(actualTime.toMillis() > EXPECTED_TIMEOUT.toMillis() / 2,
format("Handshake attempt timed out too soon (only %,d out of %,d ms)",
actualTime.toMillis(),
EXPECTED_TIMEOUT.toMillis()));
assertTrue(actualTime.toMillis() < EXPECTED_TIMEOUT.toMillis() * 2,
format("Handshake attempt timed out too late (%,d out of %,d ms)",
actualTime.toMillis(),
EXPECTED_TIMEOUT.toMillis()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.testing.tls;

import org.apache.hc.client5.testing.SSLTestContexts;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLSession;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

/**
* This test server accepts a single TLS connection request, which will then time out. The server can run in two modes.
* If {@code sendServerHello} is false, the Client Hello message will be swallowed, and the client will time out while
* waiting for the Server Hello record. Else, the server will respond to the Client Hello with a Server Hello, and the
* client's connection attempt will subsequently time out while waiting for the Change Cipher Spec record from the
* server.
*/
public class TlsHandshakeTimeoutServer implements Closeable {
private final boolean sendServerHello;

private volatile int port = -1;
private volatile boolean requestReceived = false;
private volatile ServerSocketChannel serverSocket;
private volatile SocketChannel socket;
private volatile Throwable throwable;

public TlsHandshakeTimeoutServer(final boolean sendServerHello) {
this.sendServerHello = sendServerHello;
}

public void start() throws IOException {
this.serverSocket = ServerSocketChannel.open();
this.serverSocket.bind(new InetSocketAddress("0.0.0.0", 0));
this.port = ((InetSocketAddress) this.serverSocket.getLocalAddress()).getPort();
new Thread(this::run).start();
}

private void run() {
try {
socket = serverSocket.accept();
requestReceived = true;

if (sendServerHello) {
final SSLEngine sslEngine = initHandshake();

receiveClientHello(sslEngine);
sendServerHello(sslEngine);
}
} catch (final Throwable t) {
this.throwable = t;
}
}

private SSLEngine initHandshake() throws Exception {
final SSLContext sslContext = SSLTestContexts.createServerSSLContext();
final SSLEngine sslEngine = sslContext.createSSLEngine();
// TLSv1.2 always uses a four-way handshake, which is what we want
sslEngine.setEnabledProtocols(new String[]{ "TLSv1.2" });
sslEngine.setUseClientMode(false);
sslEngine.setNeedClientAuth(false);

sslEngine.beginHandshake();
return sslEngine;
}

private void receiveClientHello(final SSLEngine sslEngine) throws IOException {
final SSLSession session = sslEngine.getSession();
final ByteBuffer clientNetData = ByteBuffer.allocate(session.getPacketBufferSize());
final ByteBuffer clientAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
while (sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) {
socket.read(clientNetData);
clientNetData.flip();
final Status status = sslEngine.unwrap(clientNetData, clientAppData).getStatus();
if (status != Status.OK) {
throw new RuntimeException("Bad status while unwrapping data: " + status);
}
clientNetData.compact();
if (sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
sslEngine.getDelegatedTask().run();
}
}
}

private void sendServerHello(final SSLEngine sslEngine) throws IOException {
final SSLSession session = sslEngine.getSession();
final ByteBuffer serverAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
final ByteBuffer serverNetData = ByteBuffer.allocate(session.getPacketBufferSize());
while (sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
serverAppData.flip();
final Status status = sslEngine.wrap(serverAppData, serverNetData).getStatus();
if (status != Status.OK) {
throw new RuntimeException("Bad status while wrapping data: " + status);
}
serverNetData.flip();
socket.write(serverNetData);
serverNetData.compact();
if (sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
sslEngine.getDelegatedTask().run();
}
}
}

public int getPort() {
if (port == -1) {
throw new IllegalStateException("Server has not been started yet");
}
return port;
}

@Override
public void close() {
try {
if (serverSocket != null) {
serverSocket.close();
}
if (socket != null) {
socket.close();
}
} catch (final IOException ignore) {
}

if (throwable != null) {
throw new RuntimeException("Exception thrown while TlsHandshakeTimerOuter was running", throwable);
} else if (!requestReceived) {
throw new IllegalStateException("Never received a request");
}
}
}