Duplicate :
|
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