TLS13HalfCloseHangTestCase.java

package io.undertow.server.ssl;

import io.undertow.Undertow;
import io.undertow.testutils.DefaultServer;
import io.undertow.util.Headers;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import org.junit.Assume;
import org.junit.Test;
import org.xnio.Options;
import org.xnio.Sequence;

public class TLS13HalfCloseHangTestCase {

  private static final Pattern CONTENT_LENGTH_PATTERN = Pattern
      .compile("Content-Length: ([0-9]+)", Pattern.CASE_INSENSITIVE);

  @Test
  public void testHang() throws IOException, GeneralSecurityException, InterruptedException {
    SSLContext clientSslContext = null;
    try {
      clientSslContext = DefaultServer.createClientSslContext("TLSv1.3");
    } catch (Throwable e) {
      // Don't try to run test if TLS 1.3 is not supported
      Assume.assumeNoException(e);
    }

    Undertow server = Undertow.builder()
        // This relies on TLSv1.2 context actually supporting TLS 1.3 which works fine with JDK11
        .addHttpsListener(0, "localhost", DefaultServer.getServerSslContext())
        .setHandler((exchange) -> {
          exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
          exchange.getResponseSender().send("Hello World!\n");
        })
        .setSocketOption(Options.SSL_ENABLED_PROTOCOLS, Sequence.of("TLSv1.3"))
        // These make the issue easier to detect
        .setIoThreads(1)
        .setWorkerThreads(1)
        .build();

    server.start();

    InetSocketAddress address = (InetSocketAddress) server.getListenerInfo().get(0).getAddress();
    String uri = "https://localhost:" + address.getPort() + "/foo";

    doRequest(clientSslContext, address);

    doRequest(clientSslContext, address);

    server.stop();
    // sleep 1 s to prevent BindException (Address already in use) when running the CI
    try {
      Thread.sleep(1000);
    } catch (InterruptedException ignore) {}
  }

  private void doRequest(SSLContext clientSslContext, InetSocketAddress address)
      throws IOException {
    Socket rawSocket = new Socket();
    rawSocket.connect(address);
    SSLSocket sslSocket = (SSLSocket) clientSslContext.getSocketFactory()
        .createSocket(rawSocket, "localhost", address.getPort(), false);
    PrintWriter writer = new PrintWriter(sslSocket.getOutputStream());
    writer.println("GET / HTTP/1.1");
    writer.println("Host: localhost");
    writer.println("Connection: keep-alive");
    writer.println();
    writer.flush();
    readResponse(sslSocket);

    sslSocket.shutdownOutput();
    rawSocket.close();
  }

  private String readLine(InputStream is) throws IOException {
    StringBuilder line = new StringBuilder();
    while (true) {
      int c = is.read();
      switch (c) {
        case -1:
          throw new RuntimeException("Unexpected EOF");
        case '\r':
          continue;
        case '\n':
          return line.toString();

        default:
          line.append((char) c);
      }
    }
  }

  private void readResponse(SSLSocket sslSocket) throws IOException {
    String line;
    int contentLength = 0;

    do {
      line = readLine(sslSocket.getInputStream());
      Matcher matcher = CONTENT_LENGTH_PATTERN.matcher(line);
      if (matcher.matches()) {
        contentLength = Integer.parseInt(matcher.group(1), 10);
      }
    } while (!line.isEmpty());

    for (int i = 0; i < contentLength; i++) {
      sslSocket.getInputStream().read();
    }
  }
}