Skip to content

Commit 09398a7

Browse files
authored
Merge pull request #354 from jruby/gem-error-minimal
SSLSocket write and non-block fixes
2 parents 726709e + 523bbe3 commit 09398a7

File tree

7 files changed

+570
-28
lines changed

7 files changed

+570
-28
lines changed

Mavenfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,13 @@ plugin :clean do
8282
'failOnError' => 'false' )
8383
end
8484

85-
jar 'org.jruby:jruby-core', '9.2.1.0', :scope => :provided
85+
jruby_compile_compat = '9.2.1.0'
86+
jar 'org.jruby:jruby-core', jruby_compile_compat, :scope => :provided
8687
# for invoker generated classes we need to add javax.annotation when on Java > 8
8788
jar 'javax.annotation:javax.annotation-api', '1.3.1', :scope => :compile
8889
jar 'org.junit.jupiter:junit-jupiter', '5.11.4', :scope => :test
90+
# a test dependency to provide digest and other stdlib bits, needed when loading OpenSSL in Java unit tests
91+
jar 'org.jruby:jruby-stdlib', jruby_compile_compat, :scope => :test
8992

9093
plugin :surefire, '3.5.5'
9194

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ DO NOT MODIFY - GENERATED CODE
113113
<version>5.11.4</version>
114114
<scope>test</scope>
115115
</dependency>
116+
<dependency>
117+
<groupId>org.jruby</groupId>
118+
<artifactId>jruby-stdlib</artifactId>
119+
<version>9.2.1.0</version>
120+
<scope>test</scope>
121+
</dependency>
116122
</dependencies>
117123
<repositories>
118124
<repository>

src/main/java/org/jruby/ext/openssl/SSLSocket.java

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,15 @@ private static CallSite callSite(final CallSite[] sites, final CallSiteIndex ind
141141
return sites[ index.ordinal() ];
142142
}
143143

144-
private SSLContext sslContext;
144+
private static final ByteBuffer EMPTY_DATA = ByteBuffer.allocate(0).asReadOnlyBuffer();
145+
146+
SSLContext sslContext;
145147
private SSLEngine engine;
146148
private RubyIO io;
147149

148-
private ByteBuffer appReadData;
149-
private ByteBuffer netReadData;
150-
private ByteBuffer netWriteData;
151-
private final ByteBuffer dummy = ByteBuffer.allocate(0); // could be static
150+
ByteBuffer appReadData;
151+
ByteBuffer netReadData;
152+
ByteBuffer netWriteData;
152153

153154
private boolean initialHandshake = false;
154155
private transient long initializeTime;
@@ -209,7 +210,7 @@ private IRubyObject fallback_set_io_nonblock_checked(ThreadContext context, Ruby
209210

210211
private static final String SESSION_SOCKET_ID = "socket_id";
211212

212-
private SSLEngine ossl_ssl_setup(final ThreadContext context, final boolean server) {
213+
SSLEngine ossl_ssl_setup(final ThreadContext context, final boolean server) {
213214
SSLEngine engine = this.engine;
214215
if ( engine != null ) return engine;
215216

@@ -553,10 +554,6 @@ private static void writeWouldBlock(final Ruby runtime, final boolean exception,
553554
result[0] = WRITE_WOULD_BLOCK_RESULT;
554555
}
555556

556-
private void doHandshake(final boolean blocking) throws IOException {
557-
doHandshake(blocking, true);
558-
}
559-
560557
// might return :wait_readable | :wait_writable in case (true, false)
561558
private IRubyObject doHandshake(final boolean blocking, final boolean exception) throws IOException {
562559
while (true) {
@@ -578,7 +575,7 @@ private IRubyObject doHandshake(final boolean blocking, final boolean exception)
578575
doTasks();
579576
break;
580577
case NEED_UNWRAP:
581-
if (readAndUnwrap(blocking) == -1 && handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED) {
578+
if (readAndUnwrap(blocking, exception) == -1 && handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED) {
582579
throw new SSLHandshakeException("Socket closed");
583580
}
584581
// during initialHandshake, calling readAndUnwrap that results UNDERFLOW does not mean writable.
@@ -614,7 +611,7 @@ private IRubyObject doHandshake(final boolean blocking, final boolean exception)
614611

615612
private void doWrap(final boolean blocking) throws IOException {
616613
netWriteData.clear();
617-
SSLEngineResult result = engine.wrap(dummy, netWriteData);
614+
SSLEngineResult result = engine.wrap(EMPTY_DATA.duplicate(), netWriteData);
618615
netWriteData.flip();
619616
handshakeStatus = result.getHandshakeStatus();
620617
status = result.getStatus();
@@ -689,7 +686,9 @@ public int write(ByteBuffer src, boolean blocking) throws SSLException, IOExcept
689686
if ( netWriteData.hasRemaining() ) {
690687
flushData(blocking);
691688
}
692-
netWriteData.clear();
689+
// compact() to preserve encrypted bytes flushData could not send (non-blocking partial write)
690+
// clear() would discard them, corrupting the TLS record stream:
691+
netWriteData.compact();
693692
final SSLEngineResult result = engine.wrap(src, netWriteData);
694693
if ( result.getStatus() == SSLEngineResult.Status.CLOSED ) {
695694
throw getRuntime().newIOError("closed SSL engine");
@@ -704,11 +703,15 @@ public int write(ByteBuffer src, boolean blocking) throws SSLException, IOExcept
704703
}
705704

706705
public int read(final ByteBuffer dst, final boolean blocking) throws IOException {
706+
return read(dst, blocking, true);
707+
}
708+
709+
private int read(final ByteBuffer dst, final boolean blocking, final boolean exception) throws IOException {
707710
if ( initialHandshake ) return 0;
708711
if ( engine.isInboundDone() ) return -1;
709712

710713
if ( ! appReadData.hasRemaining() ) {
711-
int appBytesProduced = readAndUnwrap(blocking);
714+
int appBytesProduced = readAndUnwrap(blocking, exception);
712715
if (appBytesProduced == -1 || appBytesProduced == 0) {
713716
return appBytesProduced;
714717
}
@@ -719,17 +722,16 @@ public int read(final ByteBuffer dst, final boolean blocking) throws IOException
719722
return limit;
720723
}
721724

722-
private int readAndUnwrap(final boolean blocking) throws IOException {
725+
private int readAndUnwrap(final boolean blocking, final boolean exception) throws IOException {
723726
final int bytesRead = socketChannelImpl().read(netReadData);
724727
if ( bytesRead == -1 ) {
725728
if ( ! netReadData.hasRemaining() ||
726729
( status == SSLEngineResult.Status.BUFFER_UNDERFLOW ) ) {
727730
closeInbound();
728731
return -1;
729732
}
730-
// inbound channel has been already closed but closeInbound() must
731-
// be defered till the last engine.unwrap() call.
732-
// peerNetData could not be empty.
733+
// inbound channel has been already closed but closeInbound() must be defered till
734+
// the last engine.unwrap() call; peerNetData could not be empty
733735
}
734736
appReadData.clear();
735737
netReadData.flip();
@@ -768,7 +770,7 @@ private int readAndUnwrap(final boolean blocking) throws IOException {
768770
handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_TASK ||
769771
handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP ||
770772
handshakeStatus == SSLEngineResult.HandshakeStatus.FINISHED ) ) {
771-
doHandshake(blocking);
773+
doHandshake(blocking, exception);
772774
}
773775
return appReadData.remaining();
774776
}
@@ -793,7 +795,7 @@ private void doShutdown() throws IOException {
793795
}
794796
netWriteData.clear();
795797
try {
796-
engine.wrap(dummy, netWriteData); // send close (after sslEngine.closeOutbound)
798+
engine.wrap(EMPTY_DATA.duplicate(), netWriteData); // send close (after sslEngine.closeOutbound)
797799
}
798800
catch (SSLException e) {
799801
debug(getRuntime(), "SSLSocket.doShutdown", e);
@@ -808,10 +810,10 @@ private void doShutdown() throws IOException {
808810
}
809811

810812
/**
811-
* @return the (@link RubyString} buffer or :wait_readable / :wait_writeable {@link RubySymbol}
813+
* @return the {@link RubyString} buffer or :wait_readable / :wait_writeable {@link RubySymbol}
812814
*/
813-
private IRubyObject sysreadImpl(final ThreadContext context, final IRubyObject len, final IRubyObject buff,
814-
final boolean blocking, final boolean exception) {
815+
private IRubyObject sysreadImpl(final ThreadContext context,
816+
final IRubyObject len, final IRubyObject buff, final boolean blocking, final boolean exception) {
815817
final Ruby runtime = context.runtime;
816818

817819
final int length = RubyNumeric.fix2int(len);
@@ -831,6 +833,14 @@ private IRubyObject sysreadImpl(final ThreadContext context, final IRubyObject l
831833
}
832834

833835
try {
836+
// Flush pending write data before reading (after write_nonblock encrypted bytes may still be buffered)
837+
if ( engine != null && netWriteData.hasRemaining() ) {
838+
if ( flushData(blocking) && ! blocking ) {
839+
if ( exception ) throw newSSLErrorWaitWritable(runtime, "write would block");
840+
return runtime.newSymbol("wait_writable");
841+
}
842+
}
843+
834844
// So we need to make sure to only block when there is no data left to process
835845
if ( engine == null || ! ( appReadData.hasRemaining() || netReadData.position() > 0 ) ) {
836846
final Object ex = waitSelect(SelectionKey.OP_READ, blocking, exception);
@@ -839,12 +849,12 @@ private IRubyObject sysreadImpl(final ThreadContext context, final IRubyObject l
839849

840850
final ByteBuffer dst = ByteBuffer.allocate(length);
841851
int read = -1;
842-
// ensure >0 bytes read; sysread is blocking read.
852+
// ensure > 0 bytes read; sysread is blocking read
843853
while ( read <= 0 ) {
844854
if ( engine == null ) {
845855
read = socketChannelImpl().read(dst);
846856
} else {
847-
read = read(dst, blocking);
857+
read = read(dst, blocking, exception);
848858
}
849859

850860
if ( read == -1 ) {
@@ -1226,7 +1236,7 @@ public IRubyObject ssl_version(ThreadContext context) {
12261236
return context.runtime.newString( engine.getSession().getProtocol() );
12271237
}
12281238

1229-
private transient SocketChannelImpl socketChannel;
1239+
transient SocketChannelImpl socketChannel;
12301240

12311241
private SocketChannelImpl socketChannelImpl() {
12321242
if ( socketChannel != null ) return socketChannel;
@@ -1241,7 +1251,7 @@ private SocketChannelImpl socketChannelImpl() {
12411251
throw new IllegalStateException("unknow channel impl: " + channel + " of type " + channel.getClass().getName());
12421252
}
12431253

1244-
private interface SocketChannelImpl {
1254+
interface SocketChannelImpl {
12451255

12461256
boolean isOpen() ;
12471257

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.jruby.ext.openssl;
2+
3+
import org.jruby.Ruby;
4+
import org.jruby.runtime.ThreadContext;
5+
6+
import java.io.ByteArrayOutputStream;
7+
import java.io.File;
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.nio.charset.StandardCharsets;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertTrue;
14+
15+
abstract class OpenSSLHelper {
16+
17+
protected Ruby runtime;
18+
19+
void setUpRuntime() throws ClassNotFoundException {
20+
runtime = Ruby.newInstance();
21+
loadOpenSSL(runtime);
22+
}
23+
24+
void tearDownRuntime() {
25+
if (runtime != null) runtime.tearDown(false);
26+
}
27+
28+
protected void loadOpenSSL(final Ruby runtime) throws ClassNotFoundException {
29+
// prepend lib/ so openssl.rb + jopenssl/ are loaded instead of bundled OpenSSL in jruby-stdlib
30+
final String libDir = new File("lib").getAbsolutePath();
31+
runtime.evalScriptlet("$LOAD_PATH.unshift '" + libDir + "'");
32+
runtime.evalScriptlet("require 'openssl'");
33+
34+
// sanity: verify openssl was loaded from the project, not jruby-stdlib :
35+
final String versionFile = new File(libDir, "jopenssl/version.rb").getAbsolutePath();
36+
final String expectedVersion = runtime.evalScriptlet(
37+
"File.read('" + versionFile + "').match( /.*\\sVERSION\\s*=\\s*['\"](.*)['\"]/ )[1]")
38+
.toString();
39+
final String loadedVersion = runtime.evalScriptlet("JOpenSSL::VERSION").toString();
40+
assertEquals(expectedVersion, loadedVersion, "OpenSSL must be loaded from project " +
41+
"(got version " + loadedVersion + "), not from jruby-stdlib");
42+
43+
// Also check the Java extension classes were resolved from the project, not jruby-stdlib :
44+
final String classOrigin = runtime.getJRubyClassLoader()
45+
.loadClass("org.jruby.ext.openssl.OpenSSL")
46+
.getProtectionDomain().getCodeSource().getLocation().toString();
47+
assertTrue(classOrigin.endsWith("/pkg/classes/"), "OpenSSL.class (via JRuby classloader) " +
48+
"come from project, got: " + classOrigin);
49+
}
50+
51+
// HELPERS
52+
53+
public ThreadContext currentContext() {
54+
return runtime.getCurrentContext();
55+
}
56+
57+
public static String readResource(final String resource) {
58+
int n;
59+
try (InputStream in = SSLSocketTest.class.getResourceAsStream(resource)) {
60+
if (in == null) throw new IllegalArgumentException(resource + " not found on classpath");
61+
62+
ByteArrayOutputStream out = new ByteArrayOutputStream();
63+
byte[] buf = new byte[8192];
64+
while ((n = in.read(buf)) != -1) out.write(buf, 0, n);
65+
return new String(out.toByteArray(), StandardCharsets.UTF_8);
66+
} catch (IOException e) {
67+
throw new IllegalStateException("failed to load" + resource, e);
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)