Description
The MockWebServer used by certain tests is incompatible with the way that HttpClient closes connections, when TLSv1.3 is in use AND the entire response has not been consumed. The HttpClient connection close code does some manual manipulation of the socket prior to calling close()
. Ultimately a close_notify is sent by the client and then the server attempts to respond to it, but the TLSv1.3 SSLEngine never produces any bytes and doesn't transition back to the closed state, so the MockWebServer ends up in an endless loop.
This behavior is ultimately all within JDK code on the server side; fortunately there are workarounds that we can use for tests.
- Consume the whole response
- Use
Socket#close
instead of the HttpClient closing code by creating our own connection, connection factory and connection manager
I plan to send this issue to the OpenJDK mailing list with the following as a minimal reproduction.
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsParameters;
import com.sun.net.httpserver.HttpsServer;
import javax.net.ssl.HandshakeCompletedEvent;
import javax.net.ssl.HandshakeCompletedListener;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
public class HttpsServerEndlessLoop {
public static void main(String[] args) throws Exception {
final SSLContext sslContext = getSSLContext();
InetSocketAddress address = new InetSocketAddress(InetAddress.getLoopbackAddress().getHostAddress(), 0);
HttpsServer httpsServer = HttpsServer.create(address, 0);
httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
@Override
public void configure (HttpsParameters params) {
}
});
httpsServer.createContext("/", s -> {
try {
final byte[] body = "<body>".getBytes(StandardCharsets.UTF_8);
s.sendResponseHeaders(200, body.length);
s.getResponseBody().write(body);
} finally {
s.close();
}
});
httpsServer.start();
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
SSLSocket sslSocket = (SSLSocket)
socketFactory.createSocket(httpsServer.getAddress().getHostString(), httpsServer.getAddress().getPort());
sslSocket.addHandshakeCompletedListener(new HandshakeCompletedListener() {
@Override
public void handshakeCompleted(HandshakeCompletedEvent event) {
System.out.println("handshake of initial socket completed with session: " + event.getSession());
}
});
sslSocket.startHandshake();
final String httpGetLine = "GET / HTTP/1.1\r\n\r\n";
OutputStream os = sslSocket.getOutputStream();
os.write(httpGetLine.getBytes(StandardCharsets.UTF_8));
BufferedReader reader = new BufferedReader(new InputStreamReader(sslSocket.getInputStream(), StandardCharsets.UTF_8));
// only read the first line before closing, but if all lines are read the issue doesn't happen
System.out.println(reader.readLine());
try {
try {
try {
sslSocket.shutdownOutput();
} catch (final IOException e) {
System.out.println("exception while shutting down output: ");
e.printStackTrace();
}
try {
sslSocket.shutdownInput();
} catch (final IOException e) {
System.out.println("exception while shutting down input: ");
e.printStackTrace();
}
} catch (final UnsupportedOperationException ignore) {
// if one isn't supported, the other one isn't either
}
} finally {
sslSocket.close();
}
sslSocket = (SSLSocket)
socketFactory.createSocket(httpsServer.getAddress().getHostString(), httpsServer.getAddress().getPort());
sslSocket.addHandshakeCompletedListener(new HandshakeCompletedListener() {
@Override
public void handshakeCompleted(HandshakeCompletedEvent event) {
System.out.println("handshake of second socket completed with session: " + event.getSession());
}
});
// never get past here!
sslSocket.startHandshake();
sslSocket.close();
httpsServer.stop(0);
}
private static SSLContext getSSLContext() throws Exception {
KeyStore keyStore = KeyStore.getInstance("JKS");
try (InputStream is = HttpsServerEndlessLoop.class.getResourceAsStream("/test.jks")) {
keyStore.load(is, "test".toCharArray());
}
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "test".toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
return sslContext;
}
}