Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8230788

SSL_ERROR_RX_RECORD_TOO_LONG when writing file to TCP socket via TLS

    XMLWordPrintable

    Details

    • Type: Bug
    • Status: Closed
    • Priority: P4
    • Resolution: Duplicate
    • Affects Version/s: 12.0.2
    • Fix Version/s: None
    • Component/s: core-libs
    • Labels:
    • Subcomponent:
    • CPU:
      x86_64
    • OS:
      linux

      Description

      ADDITIONAL SYSTEM INFORMATION :
      Arch Linux 5.2.9

      openjdk version "12.0.2" 2019-07-16
      OpenJDK Runtime Environment (build 12.0.2+10)
      OpenJDK 64-Bit Server VM (build 12.0.2+10, mixed mode)


      A DESCRIPTION OF THE PROBLEM :
      Writing the bytes of a JPEG file to a socket that was created from an SSLContext results in an error.
      On Firefox 69, this error is SSL_ERROR_RX_RECORD_TOO_LONG. On Chromium 76, the error is ERR_SSL_PROTOCOL_ERROR.

      The error does not occur with cURL 7.65.3: cURL downloads the image fine.

      Here's a minimal server for reproducing the issue: https://gitlab.com/bullbytes/simple_socket_based_server

      I've used Wireshark to look at the frames while the browsers are getting the image: Both Firefox and Chromium send a [FIN, ACK] frame to the server while the image is still being transmitted. The server continues sending parts of the image after which the browsers send a [RST] frame.

      These are the last couple of frames from the exchange between Firefox and the server:

      25 1.873102771 ::1 ::1 TCP 86 55444 → 8443 [ACK] Seq=937 Ack=18043 Win=56704 Len=0 TSval=3976879013 TSecr=3976879013
      26 1.873237965 ::1 ::1 TLSv1.3 110 Application Data
      27 1.873247272 ::1 ::1 TCP 86 8443 → 55444 [ACK] Seq=18043 Ack=961 Win=65536 Len=0 TSval=3976879013 TSecr=3976879013
      28 1.873346910 ::1 ::1 TCP 86 55444 → 8443 [FIN, ACK] Seq=961 Ack=18043 Win=65536 Len=0 TSval=3976879013 TSecr=3976879013
      29 1.876736432 ::1 ::1 TLSv1.3 16508 Application Data
      30 1.876769660 ::1 ::1 TCP 74 55444 → 8443 [RST] Seq=962 Win=0 Len=0


      Here's a corresponding question on Stack Overflow with a bounty on it: https://stackoverflow.com/questions/57679669/ssl-error-rx-record-too-long-with-custom-server


      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      git clone git@gitlab.com:bullbytes/simple_socket_based_server.git
      cd simple_socket_based_server
      ./gradlew run
      firefox https://localhost:8443/ada.jpg


      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      I expected to see the image in the browser.
      ACTUAL -
      SSL_ERROR_RX_RECORD_TOO_LONG in Firefox and ERR_SSL_PROTOCOL_ERROR in Chromium.

      ---------- BEGIN SOURCE ----------
      import javax.net.ssl.KeyManagerFactory;
      import javax.net.ssl.SSLContext;
      import java.io.*;
      import java.net.InetSocketAddress;
      import java.net.ServerSocket;
      import java.net.URL;
      import java.nio.charset.Charset;
      import java.nio.charset.StandardCharsets;
      import java.nio.file.Path;
      import java.security.KeyStore;
      import java.util.ArrayList;
      import java.util.Arrays;
      import java.util.List;
      import java.util.Optional;

      import static java.lang.String.format;

      /**
       * Starts the server.
       * <p>
       * Person of contact: Matthias Braun
       */
      public enum Start {
          ;

          // Used to read and write the socket's input and output stream
          private static Charset ENCODING = StandardCharsets.UTF_8;

          /**
           * Starts our server, ready to handle requests.
           *
           * @param args arguments are ignored
           */
          public static void main(String... args) {
              var address = new InetSocketAddress("0.0.0.0", 8443);

              boolean useTls = shouldUseTls(args);

              startServer(address, useTls);
          }

          private static boolean shouldUseTls(String[] args) {
              boolean useTls = true;

              for (String arg : args) {
                  if (arg.equals("--use-tls=no")) {
                      useTls = false;
                      break;
                  }
              }
              return useTls;
          }

          public static void startServer(InetSocketAddress address, boolean useTls) {

              String enabledOrDisabled = useTls ? "enabled" : "disabled";
              System.out.println(format("Starting server at %s with TLS %s", address, enabledOrDisabled));

              try (var serverSocket = useTls ?
                      getSslSocket(address) :
                      // Create a server socket without TLS
                      new ServerSocket(address.getPort(), 0, address.getAddress())) {

                  // This infinite loop is not CPU-intensive since method "accept" blocks
                  // until a client has made a connection to the socket's port
                  while (true) {
                      try (var socket = serverSocket.accept();
                           // Read the client's request from the socket
                           var requestStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                           // The server writes its response to the socket's output stream
                           var responseStream = new BufferedOutputStream(socket.getOutputStream())
                      ) {
                          System.out.println("Accepted connection on " + socket);

                          String requestedResource = getRequestedResource(requestStream)
                                  .orElse("unknown");

                          byte[] response = requestedResource.equals("/ada.jpg") ?
                                  getJpgResponse(new URL("https://upload.wikimedia.org/wikipedia/commons/a/a4/Ada_Lovelace_portrait.jpg")) :
                                  getTextResponse("The server says hi 👋", StatusCode.SUCCESS);

                          responseStream.write(response);

                          // It's important to flush the response stream before closing it to make sure any
                          // unsent bytes in the buffer are sent via the socket. Otherwise, the client gets an
                          // incomplete response
                          responseStream.flush();
                      } catch (IOException e) {
                          System.err.println("Exception while handling connection");
                          e.printStackTrace();
                      }
                  }
              } catch (Exception e) {
                  System.err.println("Could not create socket at " + address);
                  e.printStackTrace();
              }
          }

          private static Optional<String> getRequestedResource(BufferedReader requestStream) {
              var lines = getHeaderLines(requestStream);

              return first(lines).map(statusLine -> {
                  // Go past the space
                  int beginIndex = statusLine.indexOf(' ') + 1;
                  int endIndex = statusLine.lastIndexOf(' ');
                  return statusLine.substring(beginIndex, endIndex);
              });
          }

          private static <E> Optional<E> first(List<E> list) {
              return (list != null && list.size() > 0) ?
                      Optional.ofNullable(list.get(0)) :
                      Optional.empty();
          }

          private static List<String> getHeaderLines(BufferedReader reader) {

              var headerLines = new ArrayList<String>();
              try {
                  var line = reader.readLine();
                  // The header is concluded when we see an empty line.
                  // The line is null if the end of the stream was reached without reading
                  // any characters. This can happen if the client tries to connect with
                  // HTTPS while the server expects HTTP
                  while (line != null && !line.isEmpty()) {
                      headerLines.add(line);
                      line = reader.readLine();
                  }
              } catch (IOException e) {
                  System.err.println("Could not read all lines from request");
                  e.printStackTrace();
              }
              return headerLines;
          }

          private static ServerSocket getSslSocket(InetSocketAddress address)
                  throws Exception {

              // Backlog is the maximum number of pending connections on the socket, 0 means an
              // implementation-specific default is used
              int backlog = 0;

              var keyStorePath = Path.of("./tls/keystore.jks");
              char[] keyStorePassword = "pass_for_self_signed_cert".toCharArray();

              // Bind the socket to the given port and address
              var serverSocket = getSslContext(keyStorePath, keyStorePassword)
                      .getServerSocketFactory()
                      .createServerSocket(address.getPort(), backlog, address.getAddress());

              // We don't need the password anymore → Overwrite it
              Arrays.fill(keyStorePassword, '0');

              return serverSocket;
          }

          private static SSLContext getSslContext(Path keyStorePath, char[] keyStorePassword)
                  throws Exception {

              var keyStore = KeyStore.getInstance("JKS");
              keyStore.load(new FileInputStream(keyStorePath.toFile()), keyStorePassword);

              var keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
              keyManagerFactory.init(keyStore, keyStorePassword);

              var sslContext = SSLContext.getInstance("TLS");
              // Null means using default implementations for TrustManager and SecureRandom
              sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
              return sslContext;
          }

          private static byte[] concat(byte[] first, byte[] second) {
              // New array with contents of first one, having the length of the two input arrays combined
              byte[] result = Arrays.copyOf(first, first.length + second.length);
              // Copy the second array into the result array starting at the end of the first array
              System.arraycopy(second, 0, result, first.length, second.length);
              return result;
          }

          private static byte[] getJpgResponse(URL fileUrl) {

              byte[] response;
              try (var fileStream = fileUrl.openStream()) {
                  var imageBytes = fileStream.readAllBytes();
                  var fileName = new File(fileUrl.getPath()).getName();

                  var statusLine = "HTTP/1.1 200 OK";
                  var contentLength = "Content-Length: " + imageBytes.length;
                  var contentType = "Content-Type: image/jpeg";
                  var contentDisposition = format("Content-Disposition: inline; filename=%s", fileName);

                  String header = statusLine + "\r\n" +
                          contentLength + "\r\n" +
                          contentType + "\r\n" +
                          contentDisposition + "\r\n" +
                          "\r\n";

                  // Append the bytes of the image to the bytes of the header
                  response = concat(header.getBytes(ENCODING), imageBytes);

              } catch (IOException e) {
                  var msg = format("Could not read file at URL '%s'", fileUrl);
                  System.err.println(msg);
                  response = getTextResponse(msg, StatusCode.SERVER_ERROR);
              }
              return response;
          }

          private static byte[] getTextResponse(String text, StatusCode status) {
              var body = text + "\r\n";
              var contentLength = body.getBytes(ENCODING).length;
              var statusLine = format("HTTP/1.1 %s %s\r\n", status.code, status.text);

              var response = statusLine +
                      format("Content-Length: %d\r\n", contentLength) +
                      format("Content-Type: text/plain; charset=%s\r\n",
                              ENCODING.displayName()) +
                      "\r\n" +
                      body;

              return response.getBytes(ENCODING);
          }

          /**
           * HTTP status codes such as 200 and 404.
           * <p>
           * Person of contact: Matthias Braun
           */
          public enum StatusCode {
              SUCCESS(200, "Success"),
              SERVER_ERROR(500, "Internal Server Error");

              private final int code;
              private final String text;

              StatusCode(int code, String text) {
                  this.text = text;
                  this.code = code;
              }

              public int getCode() {
                  return code;
              }

              public String getText() {
                  return text;
              }

              /**
               * @return "200 Success" or "404 Not Found", for example
               */
              @Override
              public String toString() {
                  return code + " " + text;
              }
          }
      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      The described issue only occurs with TLS enabled. When starting the example server without TLS, the image is served just fine:

      ./gradlew run --args="--use-tls=no"
      firefox http://localhost:8443/ada.jpg

      Also, getting the image via TLS works when using cURL:
      curl -Ok "https://localhost:8443/ada.jpg"

      FREQUENCY : always


        Attachments

          Issue Links

            Activity

              People

              Assignee:
              psonal Pallavi Sonal (Inactive)
              Reporter:
              webbuggrp Webbug Group
              Votes:
              0 Vote for this issue
              Watchers:
              1 Start watching this issue

                Dates

                Created:
                Updated:
                Resolved: