JDK-8230788 : SSL_ERROR_RX_RECORD_TOO_LONG when writing file to TCP socket via TLS
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.net
  • Affected Version: 12.0.2
  • Priority: P4
  • Status: Closed
  • Resolution: Duplicate
  • OS: linux
  • CPU: x86_64
  • Submitted: 2019-09-09
  • Updated: 2019-09-10
  • Resolved: 2019-09-10
Related Reports
Duplicate :  
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