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
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
- duplicates
-
JDK-8225714 TLSv1.3 may generate TLSInnerPlainText longer than 2^14+1 bytes
-
- Resolved
-