Skip to content

Commit 5eab728

Browse files
committed
support (MRI 2.2) exception: false with syswrite_nonblock and sysread_nonblock
... in this case WaitReadable/WaitWritable errors won't be raised also implemented `npn_protocol` as dummy method for compatibility only
1 parent 36f87ad commit 5eab728

File tree

1 file changed

+137
-58
lines changed

1 file changed

+137
-58
lines changed

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

Lines changed: 137 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.jruby.Ruby;
5252
import org.jruby.RubyArray;
5353
import org.jruby.RubyClass;
54+
import org.jruby.RubyHash;
5455
import org.jruby.RubyIO;
5556
import org.jruby.RubyModule;
5657
import org.jruby.RubyNumeric;
@@ -356,9 +357,10 @@ public IRubyObject verify_result(final ThreadContext context) {
356357
// SelectableChannel.configureBlocking(false) permanently instead of setting
357358
// temporarily. SSLSocket requires wrapping IO to be selectable so it should
358359
// be OK to set configureBlocking(false) permanently.
359-
private boolean waitSelect(final int operations, final boolean blocking) throws IOException {
360+
private Object waitSelect(final int operations, final boolean blocking, final boolean exception)
361+
throws IOException {
360362
final SocketChannelImpl channel = socketChannelImpl();
361-
if ( ! channel.isSelectable() ) return true;
363+
if ( ! channel.isSelectable() ) return Boolean.TRUE;
362364

363365
final Ruby runtime = getRuntime();
364366
final RubyThread thread = runtime.getCurrentContext().getThread();
@@ -381,16 +383,20 @@ public void run() throws InterruptedException {
381383
if ( result[0] == 0 ) {
382384
if ((operations & SelectionKey.OP_READ) != 0 && (operations & SelectionKey.OP_WRITE) != 0) {
383385
if ( key.isReadable() ) {
384-
writeWouldBlock(runtime);
385-
} else if ( key.isWritable() ) {
386-
readWouldBlock(runtime);
387-
} else { //neither, pick one
388-
readWouldBlock(runtime);
386+
writeWouldBlock(runtime, exception, result); return;
389387
}
390-
} else if ((operations & SelectionKey.OP_READ) != 0) {
391-
readWouldBlock(runtime);
392-
} else if ((operations & SelectionKey.OP_WRITE) != 0) {
393-
writeWouldBlock(runtime);
388+
//else if ( key.isWritable() ) {
389+
// readWouldBlock(runtime, exception, result);
390+
//}
391+
else { //neither, pick one
392+
readWouldBlock(runtime, exception, result); return;
393+
}
394+
}
395+
else if ((operations & SelectionKey.OP_READ) != 0) {
396+
readWouldBlock(runtime, exception, result); return;
397+
}
398+
else if ((operations & SelectionKey.OP_WRITE) != 0) {
399+
writeWouldBlock(runtime, exception, result); return;
394400
}
395401
}
396402
}
@@ -408,16 +414,21 @@ public void wakeup() {
408414
}
409415
});
410416

411-
if ( result[0] >= 1 ) {
412-
Set<SelectionKey> keySet = selector.selectedKeys();
413-
if ( keySet.iterator().next() == key ) return true;
417+
switch ( result[0] ) {
418+
case READ_WOULD_BLOCK_RESULT :
419+
return runtime.newSymbol("wait_readable"); // exception: false
420+
case WRITE_WOULD_BLOCK_RESULT :
421+
return runtime.newSymbol("wait_writable"); // exception: false
422+
case 0 : return Boolean.FALSE;
423+
default :
424+
if ( result[0] >= 1 ) {
425+
Set<SelectionKey> keySet = selector.selectedKeys();
426+
if ( keySet.iterator().next() == key ) return Boolean.TRUE;
427+
}
428+
return Boolean.FALSE;
414429
}
415-
416-
return false;
417-
}
418-
catch (InterruptedException ie) {
419-
return false;
420430
}
431+
catch (InterruptedException interrupt) { return Boolean.FALSE; }
421432
finally {
422433
// Note: I don't like ignoring these exceptions, but it's
423434
// unclear how likely they are to happen or what damage we
@@ -452,17 +463,22 @@ public void wakeup() {
452463
}
453464
}
454465

455-
private static void readWouldBlock(final Ruby runtime) {
456-
throw newSSLErrorWaitReadable(runtime, "read would block");
466+
private static final int READ_WOULD_BLOCK_RESULT = Integer.MIN_VALUE + 1;
467+
private static final int WRITE_WOULD_BLOCK_RESULT = Integer.MIN_VALUE + 2;
468+
469+
private static void readWouldBlock(final Ruby runtime, final boolean exception, final int[] result) {
470+
if ( exception ) throw newSSLErrorWaitReadable(runtime, "read would block");
471+
result[0] = READ_WOULD_BLOCK_RESULT;
457472
}
458473

459-
private static void writeWouldBlock(final Ruby runtime) {
460-
throw newSSLErrorWaitWritable(runtime, "write would block");
474+
private static void writeWouldBlock(final Ruby runtime, final boolean exception, final int[] result) {
475+
if ( exception ) throw newSSLErrorWaitWritable(runtime, "write would block");
476+
result[0] = WRITE_WOULD_BLOCK_RESULT;
461477
}
462478

463479
private void doHandshake(final boolean blocking) throws IOException {
464480
while (true) {
465-
boolean ready = waitSelect(SelectionKey.OP_READ | SelectionKey.OP_WRITE, blocking);
481+
boolean ready = waitSelect(SelectionKey.OP_READ | SelectionKey.OP_WRITE, blocking, true) == Boolean.TRUE;
466482

467483
// if not blocking, raise EAGAIN
468484
if ( ! blocking && ! ready ) {
@@ -487,7 +503,7 @@ private void doHandshake(final boolean blocking) throws IOException {
487503
// does not mean writable. we explicitly wait for readable channel to avoid
488504
// busy loop.
489505
if (initialHandshake && status == SSLEngineResult.Status.BUFFER_UNDERFLOW) {
490-
waitSelect(SelectionKey.OP_READ, blocking);
506+
waitSelect(SelectionKey.OP_READ, blocking, true);
491507
}
492508
break;
493509
case NEED_WRAP:
@@ -669,33 +685,34 @@ private void doShutdown() throws IOException {
669685
flushData(true);
670686
}
671687

672-
private RubyString sysreadImpl(final ThreadContext context,
673-
final IRubyObject[] args, final boolean blocking) {
688+
private IRubyObject do_sysread(final ThreadContext context,
689+
IRubyObject len, IRubyObject buff, final boolean blocking, final boolean exception) {
674690
final Ruby runtime = context.runtime;
675691

676-
final int len = RubyNumeric.fix2int(args[0]);
677-
final RubyString buff;
692+
final int length = RubyNumeric.fix2int(len);
693+
final RubyString buffStr;
678694

679-
if ( args.length == 2 && ! args[1].isNil() ) {
680-
buff = args[1].asString();
695+
if ( buff != null && ! buff.isNil() ) {
696+
buffStr = buff.asString();
681697
} else {
682-
buff = runtime.newString();
698+
buffStr = RubyString.newEmptyString(runtime); // fine since we're setValue
683699
}
684-
if ( len == 0 ) {
685-
buff.clear();
686-
return buff;
700+
if ( length == 0 ) {
701+
buffStr.clear();
702+
return buffStr;
687703
}
688-
if ( len < 0 ) {
704+
if ( length < 0 ) {
689705
throw runtime.newArgumentError("negative string size (or size too big)");
690706
}
691707

692708
try {
693709
// So we need to make sure to only block when there is no data left to process
694710
if ( engine == null || ! ( peerAppData.hasRemaining() || peerNetData.position() > 0 ) ) {
695-
waitSelect(SelectionKey.OP_READ, blocking);
711+
final Object ex = waitSelect(SelectionKey.OP_READ, blocking, exception);
712+
if ( ex instanceof IRubyObject ) return (IRubyObject) ex; // :wait_readable
696713
}
697714

698-
ByteBuffer dst = ByteBuffer.allocate(len);
715+
ByteBuffer dst = ByteBuffer.allocate(length);
699716
int rr = -1;
700717
// ensure >0 bytes read; sysread is blocking read.
701718
while ( rr <= 0 ) {
@@ -712,46 +729,92 @@ private RubyString sysreadImpl(final ThreadContext context,
712729
// instead of spinning until the rest comes in, call waitSelect to either block
713730
// until the rest is available, or throw a "read would block" error if we are in
714731
// non-blocking mode.
715-
waitSelect(SelectionKey.OP_READ, blocking);
732+
final Object ex = waitSelect(SelectionKey.OP_READ, blocking, exception);
733+
if ( ex instanceof IRubyObject ) return (IRubyObject) ex; // :wait_readable
716734
}
717735
}
718736
byte[] bss = new byte[rr];
719737
dst.position(dst.position() - rr);
720738
dst.get(bss);
721-
buff.setValue(new ByteList(bss, false));
722-
return buff;
739+
buffStr.setValue(new ByteList(bss, false));
740+
return buffStr;
723741
}
724742
catch (IOException ioe) {
725743
throw runtime.newIOError(ioe.getMessage());
726744
}
727745
}
728746

729-
@JRubyMethod(rest = true, required = 1, optional = 1)
747+
@JRubyMethod
748+
public IRubyObject sysread(ThreadContext context, IRubyObject len) {
749+
return do_sysread(context, len, null, true, true);
750+
}
751+
752+
@JRubyMethod
753+
public IRubyObject sysread(ThreadContext context, IRubyObject len, IRubyObject buff) {
754+
return do_sysread(context, len, buff, true, true);
755+
}
756+
757+
@Deprecated // @JRubyMethod(rest = true, required = 1, optional = 1)
730758
public IRubyObject sysread(ThreadContext context, IRubyObject[] args) {
731-
return sysreadImpl(context, args, true);
759+
switch ( args.length) {
760+
case 1 :
761+
return sysread(context, args[0]);
762+
case 2 :
763+
return sysread(context, args[0], args[1]);
764+
}
765+
Arity.checkArgumentCount(context.runtime, args.length, 1, 2);
766+
return null; // won't happen as checkArgumentCount raises
767+
}
768+
769+
@JRubyMethod
770+
public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject len) {
771+
return do_sysread(context, len, null, false, true);
732772
}
733773

734-
@JRubyMethod(rest = true, required = 1, optional = 2)
774+
@JRubyMethod
775+
public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject len, IRubyObject arg) {
776+
if ( arg instanceof RubyHash ) { // exception: false
777+
return do_sysread(context, len, null, false, getExceptionOpt(context, arg));
778+
}
779+
return do_sysread(context, len, arg, false, true); // buffer arg
780+
}
781+
782+
@JRubyMethod
783+
public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject len, IRubyObject buff, IRubyObject opts) {
784+
return do_sysread(context, len, buff, false, getExceptionOpt(context, opts));
785+
}
786+
787+
788+
@Deprecated // @JRubyMethod(rest = true, required = 1, optional = 2)
735789
public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject[] args) {
736-
// TODO: options for exception raising
737-
return sysreadImpl(context, args, false);
790+
switch ( args.length) {
791+
case 1 :
792+
return sysread_nonblock(context, args[0]);
793+
case 2 :
794+
return sysread_nonblock(context, args[0], args[1]);
795+
case 3 :
796+
return sysread_nonblock(context, args[0], args[1], args[2]);
797+
}
798+
Arity.checkArgumentCount(context.runtime, args.length, 1, 3);
799+
return null; // won't happen as checkArgumentCount raises
738800
}
739801

740802
private IRubyObject do_syswrite(final ThreadContext context,
741-
final IRubyObject arg, final boolean blocking) {
803+
final IRubyObject arg, final boolean blocking, final boolean exception) {
742804
final Ruby runtime = context.runtime;
743805
try {
744806
checkClosed();
745807

746-
waitSelect(SelectionKey.OP_WRITE, blocking);
808+
final Object ex = waitSelect(SelectionKey.OP_WRITE, blocking, exception);
809+
if ( ex instanceof IRubyObject ) return (IRubyObject) ex; // :wait_writable
747810

748-
ByteList bls = arg.asString().getByteList();
749-
ByteBuffer b1 = ByteBuffer.wrap(bls.getUnsafeBytes(), bls.getBegin(), bls.getRealSize());
811+
ByteList bytes = arg.asString().getByteList();
812+
ByteBuffer buff = ByteBuffer.wrap(bytes.getUnsafeBytes(), bytes.getBegin(), bytes.getRealSize());
750813
final int written;
751814
if ( engine == null ) {
752-
written = writeToChannel(b1, blocking);
815+
written = writeToChannel(buff, blocking);
753816
} else {
754-
written = write(b1, blocking);
817+
written = write(buff, blocking);
755818
}
756819

757820
this.io.callMethod(context, "flush");
@@ -765,18 +828,26 @@ private IRubyObject do_syswrite(final ThreadContext context,
765828

766829
@JRubyMethod
767830
public IRubyObject syswrite(ThreadContext context, IRubyObject arg) {
768-
return do_syswrite(context, arg, true);
831+
return do_syswrite(context, arg, true, true);
769832
}
770833

771834
@JRubyMethod
772835
public IRubyObject syswrite_nonblock(ThreadContext context, IRubyObject arg) {
773-
return do_syswrite(context, arg, false);
836+
return do_syswrite(context, arg, false, true);
774837
}
775838

776839
@JRubyMethod
777-
public IRubyObject syswrite_nonblock(ThreadContext context, IRubyObject arg, IRubyObject options) {
778-
// TODO: options for exception raising
779-
return do_syswrite(context, arg, false);
840+
public IRubyObject syswrite_nonblock(ThreadContext context, IRubyObject arg, IRubyObject opts) {
841+
return do_syswrite(context, arg, false, getExceptionOpt(context, opts));
842+
}
843+
844+
private static boolean getExceptionOpt(final ThreadContext context, final IRubyObject opts) {
845+
if ( opts instanceof RubyHash ) { // exception: true
846+
final Ruby runtime = context.runtime;
847+
IRubyObject exc = ((RubyHash) opts).op_aref(context, runtime.newSymbol("exception"));
848+
return exc != runtime.getFalse();
849+
}
850+
return true;
780851
}
781852

782853
private void checkClosed() {
@@ -887,6 +958,14 @@ public IRubyObject cipher() {
887958
return getRuntime().newString( engine.getSession().getCipherSuite() );
888959
}
889960

961+
@JRubyMethod
962+
public IRubyObject npn_protocol() {
963+
if ( engine == null ) return getRuntime().getNil();
964+
// NOTE: maybe a time to use https://github.com/benmmurphy/ssl_npn
965+
warn(getRuntime().getCurrentContext(), "WARNING: SSLSocket#npn_protocol is not supported");
966+
return getRuntime().getNil(); // throw new UnsupportedOperationException();
967+
}
968+
890969
@JRubyMethod
891970
public IRubyObject state() {
892971
warn(getRuntime().getCurrentContext(), "WARNING: unimplemented method called: SSLSocket#state");
@@ -905,7 +984,7 @@ public IRubyObject session_reused_p() {
905984
return getRuntime().getNil(); // throw new UnsupportedOperationException();
906985
}
907986

908-
javax.net.ssl.SSLSession getSession() {
987+
final javax.net.ssl.SSLSession getSession() {
909988
return engine == null ? null : engine.getSession();
910989
}
911990

0 commit comments

Comments
 (0)