diff --git a/src/main/java/org/jruby/ext/openssl/SSLSocket.java b/src/main/java/org/jruby/ext/openssl/SSLSocket.java index fdeedf8c..4744c8e7 100644 --- a/src/main/java/org/jruby/ext/openssl/SSLSocket.java +++ b/src/main/java/org/jruby/ext/openssl/SSLSocket.java @@ -863,9 +863,9 @@ public int read(final ByteBuffer dst, final boolean blocking) throws IOException private int read(final ByteBuffer dst, final boolean blocking, final boolean exception) throws IOException { if ( initialHandshake ) return 0; - if ( engine.isInboundDone() ) return -1; if ( ! appReadData.hasRemaining() ) { + if ( engine.isInboundDone() ) return -1; final int appBytesProduced = readAndUnwrap(blocking, exception); if (appBytesProduced == -1 || appBytesProduced == 0 || isWouldBlockResult(appBytesProduced)) { return appBytesProduced; @@ -880,7 +880,7 @@ private int read(final ByteBuffer dst, final boolean blocking, final boolean exc private int readAndUnwrap(final boolean blocking, final boolean exception) throws IOException { final int bytesRead = socketChannelImpl().read(netReadData); if ( bytesRead == -1 ) { - if ( ! netReadData.hasRemaining() || + if ( netReadData.position() == 0 || ( status == SSLEngineResult.Status.BUFFER_UNDERFLOW ) ) { closeInbound(); return -1; @@ -916,16 +916,16 @@ private int readAndUnwrap(final boolean blocking, final boolean exception) throw status = result.getStatus(); handshakeStatus = result.getHandshakeStatus(); - if ( bytesRead == -1 && ! netReadData.hasRemaining() ) { - // now it's safe to call closeInbound(). + if ( bytesRead == -1 && ! netReadData.hasRemaining() && appReadData.position() == 0 ) { + // Deliver decrypted data first; report an unclean EOF on the next read. closeInbound(); } - if ( status == SSLEngineResult.Status.CLOSED ) { - return -1; - } netReadData.compact(); appReadData.flip(); + if ( status == SSLEngineResult.Status.CLOSED && ! appReadData.hasRemaining() ) { + return -1; + } if ( ! initialHandshake && ( handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_TASK || handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP || diff --git a/test/ssl/test_ssl.rb b/test/ssl/test_ssl.rb index bd0ef69b..c69064bc 100644 --- a/test/ssl/test_ssl.rb +++ b/test/ssl/test_ssl.rb @@ -489,6 +489,48 @@ def test_unexpected_eof_is_not_accepted_by_default tcp_server&.close rescue nil end + def test_unexpected_eof_delivers_buffered_data_first + [:sysread, :read_nonblock].each do |read_method| + server_ctx = OpenSSL::SSL::SSLContext.new + server_ctx.cert = @svr_cert + server_ctx.key = @svr_key + server_ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + tcp_server = TCPServer.new("127.0.0.1", 0) + ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, server_ctx) + records = ["first record", "second record"] + + server_thread = Thread.new do + peer = ssl_server.accept + records.each { |record| peer.write(record) } + peer.flush + peer.to_io.close + end + + begin + client_ctx = OpenSSL::SSL::SSLContext.new + client_ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE + client_ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + sock = TCPSocket.new("127.0.0.1", tcp_server.addr[1]) + ssl = OpenSSL::SSL::SSLSocket.new(sock, client_ctx) + ssl.connect + # Queue both TLS records and the transport EOF before reading any plaintext. + server_thread.value + + payload = records.join + received = +"" + received << ssl.public_send(read_method, 1) until received.bytesize == payload.bytesize + assert_equal payload, received + assert_raise(OpenSSL::SSL::SSLError) { ssl.public_send(read_method, 1) } + ensure + ssl&.close rescue nil + sock&.close rescue nil + server_thread&.join rescue nil + ssl_server&.close rescue nil + tcp_server&.close rescue nil + end + end + end + def test_unexpected_eof_can_be_ignored_with_option server_ctx = OpenSSL::SSL::SSLContext.new server_ctx.cert = @svr_cert