/***** BEGIN LICENSE BLOCK ***** * Version: EPL 1.0/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Eclipse Public * License Version 1.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.eclipse.org/legal/epl-v10.html * * Software distributed under the License is distributed on an "AS * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or * implied. See the License for the specific language governing * rights and limitations under the License. * * Copyright (C) 2006, 2007 Ola Bini <ola@ologix.com> * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the EPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the EPL, the GPL or the LGPL. ***** END LICENSE BLOCK *****/ package org.jruby.ext.openssl; import java.io.IOException; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.Channel; import java.nio.channels.ClosedChannelException; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLPeerUnverifiedException; import org.jruby.Ruby; import org.jruby.RubyArray; import org.jruby.RubyClass; import org.jruby.RubyHash; import org.jruby.RubyIO; import org.jruby.RubyModule; import org.jruby.RubyNumeric; import org.jruby.RubyObject; import org.jruby.RubyString; import org.jruby.RubyThread; import org.jruby.anno.JRubyMethod; import org.jruby.exceptions.RaiseException; import org.jruby.ext.openssl.x509store.X509Utils; import org.jruby.runtime.Arity; import org.jruby.runtime.Block; import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.util.ByteList; import org.jruby.runtime.Visibility; import static org.jruby.ext.openssl.SSL.newSSLErrorWaitReadable; import static org.jruby.ext.openssl.SSL.newSSLErrorWaitWritable; import static org.jruby.ext.openssl.OpenSSL.*; /** * @author <a href="mailto:ola.bini@ki.se">Ola Bini</a> */ public class SSLSocket extends RubyObject { private static final long serialVersionUID = -2084816623554406237L; private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() { public IRubyObject allocate(Ruby runtime, RubyClass klass) { return new SSLSocket(runtime, klass); } }; public static void createSSLSocket(final Ruby runtime, final RubyModule SSL) { // OpenSSL::SSL final ThreadContext context = runtime.getCurrentContext(); RubyClass SSLSocket = SSL.defineClassUnder("SSLSocket", runtime.getObject(), ALLOCATOR); // SSLSocket.addReadAttribute(context, "io"); // SSLSocket.defineAlias("to_io", "io"); // SSLSocket.addReadAttribute(context, "context"); SSLSocket.addReadWriteAttribute(context, "sync_close"); SSLSocket.addReadWriteAttribute(context, "hostname"); SSLSocket.defineAnnotatedMethods(SSLSocket.class); SSLSocket.undefineMethod("dup"); } public SSLSocket(Ruby runtime, RubyClass type) { super(runtime, type); } private static RaiseException newSSLError(Ruby runtime, Exception exception) { return SSL.newSSLError(runtime, exception); } private static RaiseException newSSLError(Ruby runtime, String message) { return SSL.newSSLError(runtime, message); } private static RaiseException newSSLErrorFromHandshake(Ruby runtime, SSLHandshakeException exception) { // SSLHandshakeException is always a wrap around another exception that // is the actual cause. In some cases the diagnostic message from the original // exception is also lost and the handshake exception reads "General SSLEngine problem" // Follow the cause chain until we get the real message and use that to ensure // we raise an exception that contains the real reason for failure Exception cause = exception; while (cause.getCause() != null && (cause instanceof SSLHandshakeException)) { cause = (Exception) cause.getCause(); } return SSL.newSSLError(runtime, cause); } private SSLContext sslContext; private SSLEngine engine; private RubyIO io; private ByteBuffer peerAppData; private ByteBuffer peerNetData; private ByteBuffer netData; private ByteBuffer dummy; private boolean initialHandshake = false; private SSLEngineResult.HandshakeStatus handshakeStatus; private SSLEngineResult.Status status; int verifyResult = X509Utils.V_OK; @Deprecated public IRubyObject _initialize(final ThreadContext context, final IRubyObject[] args, final Block unused) { return initialize(context, args); } @JRubyMethod(name = "initialize", rest = true, frame = true, visibility = Visibility.PRIVATE) public IRubyObject initialize(final ThreadContext context, final IRubyObject[] args) { final Ruby runtime = context.runtime; if ( Arity.checkArgumentCount(runtime, args, 1, 2) == 1 ) { sslContext = new SSLContext(runtime).initializeImpl(); } else { sslContext = (SSLContext) args[1]; } if ( ! ( args[0] instanceof RubyIO ) ) { throw runtime.newTypeError("IO expected but got " + args[0].getMetaClass().getName()); } setInstanceVariable("@io", this.io = (RubyIO) args[0]); // compat (we do not read @io) // Ruby 2.3 : @io.nonblock = true if @io.respond_to?(:nonblock=) if (io.respondsTo("nonblock=")) { io.callMethod(context, "nonblock=", runtime.getTrue()); } setInstanceVariable("@context", this.sslContext); // only compat (we do not use @context) // This is a bit of a hack: SSLSocket should share code with // RubyBasicSocket, which always sets sync to true. // Instead we set it here for now. this.set_sync(context, runtime.getTrue()); // io.sync = true this.callMethod(context, "sync_close=", runtime.getFalse()); sslContext.setup(context); return Utils.invokeSuper(context, this, args, Block.NULL_BLOCK); // super() } private SSLEngine ossl_ssl_setup(final ThreadContext context) throws NoSuchAlgorithmException, KeyManagementException, IOException { SSLEngine engine = this.engine; if ( engine != null ) return engine; // Server Name Indication (SNI) RFC 3546 // SNI support will not be attempted unless hostname is explicitly set by the caller String peerHost = this.callMethod(context, "hostname").toString(); final int peerPort = socketChannelImpl().getRemotePort(); engine = sslContext.createSSLEngine(peerHost, peerPort); final javax.net.ssl.SSLSession session = engine.getSession(); peerNetData = ByteBuffer.allocate(session.getPacketBufferSize()); peerAppData = ByteBuffer.allocate(session.getApplicationBufferSize()); netData = ByteBuffer.allocate(session.getPacketBufferSize()); peerNetData.limit(0); peerAppData.limit(0); netData.limit(0); dummy = ByteBuffer.allocate(0); this.engine = engine; copySessionSetupIfSet(); return engine; } @JRubyMethod(name = "io", alias = "to_io") public final RubyIO io() { return this.io; } @JRubyMethod(name = "context") public final SSLContext context() { return this.sslContext; } @JRubyMethod(name = "sync") public IRubyObject sync(final ThreadContext context) { return this.io.callMethod(context, "sync"); } @JRubyMethod(name = "sync=") public IRubyObject set_sync(final ThreadContext context, final IRubyObject sync) { return this.io.callMethod(context, "sync=", sync); } @JRubyMethod public IRubyObject connect(final ThreadContext context) { return connectImpl(context, true, true); } @JRubyMethod public IRubyObject connect_nonblock(final ThreadContext context) { return connectImpl(context, false, true); } @JRubyMethod public IRubyObject connect_nonblock(final ThreadContext context, IRubyObject opts) { return connectImpl(context, false, getExceptionOpt(context, opts)); } private IRubyObject connectImpl(final ThreadContext context, final boolean blocking, final boolean exception) { if ( ! sslContext.isProtocolForClient() ) { throw newSSLError(context.runtime, "called a function you should not call"); } try { if ( ! initialHandshake ) { SSLEngine engine = ossl_ssl_setup(context); engine.setUseClientMode(true); engine.beginHandshake(); handshakeStatus = engine.getHandshakeStatus(); initialHandshake = true; } callRenegotiationCallback(context); final IRubyObject ex = doHandshake(blocking, exception); if ( ex != null ) return ex; // :wait_readable | :wait_writable } catch (SSLHandshakeException e) { //debugStackTrace(runtime, e); // unlike server side, client should close outbound channel even if // we have remaining data to be sent. forceClose(); throw newSSLErrorFromHandshake(context.runtime, e); } catch (NoSuchAlgorithmException e) { debugStackTrace(context.runtime, e); forceClose(); throw newSSLError(context.runtime, e); } catch (KeyManagementException e) { debugStackTrace(context.runtime, e); forceClose(); throw newSSLError(context.runtime, e); } catch (IOException e) { //debugStackTrace(context.runtime, e); forceClose(); throw newSSLError(context.runtime, e); } catch (NotYetConnectedException e) { throw newErrnoEPIPEError(context.runtime, "SSL_connect"); } return this; } private static RaiseException newErrnoEPIPEError(final Ruby runtime, final String detail) { return Utils.newError(runtime, runtime.getErrno().getClass("EPIPE"), detail); } @JRubyMethod public IRubyObject accept(final ThreadContext context) { return acceptImpl(context, true, true); } @JRubyMethod public IRubyObject accept_nonblock(final ThreadContext context) { return acceptImpl(context, false, true); } @JRubyMethod public IRubyObject accept_nonblock(final ThreadContext context, IRubyObject opts) { return acceptImpl(context, false, getExceptionOpt(context, opts)); } @Deprecated public SSLSocket acceptCommon(ThreadContext context, boolean blocking) { return (SSLSocket) acceptImpl(context, blocking, true); } private IRubyObject acceptImpl(final ThreadContext context, final boolean blocking, final boolean exception) { if ( ! sslContext.isProtocolForServer() ) { throw newSSLError(context.runtime, "called a function you should not call"); } try { if ( ! initialHandshake ) { final SSLEngine engine = ossl_ssl_setup(context); engine.setUseClientMode(false); final IRubyObject verify_mode = sslContext.callMethod(context, "verify_mode"); if ( ! verify_mode.isNil() ) { final int verify = RubyNumeric.fix2int(verify_mode); if ( verify == 0 ) { // VERIFY_NONE engine.setNeedClientAuth(false); engine.setWantClientAuth(false); } if ( ( verify & 1 ) != 0 ) { // VERIFY_PEER engine.setWantClientAuth(true); } if ( ( verify & 2 ) != 0 ) { // VERIFY_FAIL_IF_NO_PEER_CERT engine.setNeedClientAuth(true); } } engine.beginHandshake(); handshakeStatus = engine.getHandshakeStatus(); initialHandshake = true; } callRenegotiationCallback(context); final IRubyObject ex = doHandshake(blocking, exception); if ( ex != null ) return ex; // :wait_readable | :wait_writable } catch (SSLHandshakeException e) { final String msg = e.getMessage(); // updated JDK (>= 1.7.0_75) with deprecated SSL protocols : // javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate) if ( e.getCause() == null && msg != null && msg.contains("(protocol is disabled or cipher suites are inappropriate)") ) { debug(context.runtime, sslContext.getProtocol() + " protocol has been deactivated and is not available by default\n see the java.security.Security property jdk.tls.disabledAlgorithms in <JRE_HOME>/lib/security/java.security file"); } else { debugStackTrace(context.runtime, e); } throw newSSLErrorFromHandshake(context.runtime, e); } catch (NoSuchAlgorithmException e) { debugStackTrace(context.runtime, e); throw newSSLError(context.runtime, e); } catch (KeyManagementException e) { debugStackTrace(context.runtime, e); throw newSSLError(context.runtime, e); } catch (IOException e) { debugStackTrace(context.runtime, e); throw newSSLError(context.runtime, e); } catch (RaiseException e) { throw e; } catch (RuntimeException e) { debugStackTrace(context.runtime, e); if ( "Could not generate DH keypair".equals( e.getMessage() ) ) { throw SSL.handleCouldNotGenerateDHKeyPairError(context.runtime, e); } throw newSSLError(context.runtime, e); } return this; } @JRubyMethod public IRubyObject verify_result(final ThreadContext context) { final Ruby runtime = context.runtime; if (engine == null) { runtime.getWarnings().warn("SSL session is not started yet."); return runtime.getNil(); } return runtime.newFixnum(verifyResult); } // This select impl is a copy of RubyThread.select, then blockingLock is // removed. This impl just set // SelectableChannel.configureBlocking(false) permanently instead of setting // temporarily. SSLSocket requires wrapping IO to be selectable so it should // be OK to set configureBlocking(false) permanently. private Object waitSelect(final int operations, final boolean blocking, final boolean exception) throws IOException { final SocketChannelImpl channel = socketChannelImpl(); if ( ! channel.isSelectable() ) return Boolean.TRUE; final Ruby runtime = getRuntime(); final RubyThread thread = runtime.getCurrentContext().getThread(); channel.configureBlocking(false); final Selector selector = runtime.getSelectorPool().get(); final SelectionKey key = channel.register(selector, operations); try { final int[] result = new int[1]; if ( ! blocking ) { try { result[0] = selector.selectNow(); if ( result[0] == 0 ) { if ((operations & SelectionKey.OP_READ) != 0 && (operations & SelectionKey.OP_WRITE) != 0) { if ( key.isReadable() ) { writeWouldBlock(runtime, exception, result); } //else if ( key.isWritable() ) { // readWouldBlock(runtime, exception, result); //} else { //neither, pick one readWouldBlock(runtime, exception, result); } } else if ((operations & SelectionKey.OP_READ) != 0) { readWouldBlock(runtime, exception, result); } else if ((operations & SelectionKey.OP_WRITE) != 0) { writeWouldBlock(runtime, exception, result); } } } catch (IOException ioe) { throw runtime.newRuntimeError("Error with selector: " + ioe.getMessage()); } } else { io.addBlockingThread(thread); thread.executeBlockingTask(new RubyThread.BlockingTask() { public void run() throws InterruptedException { try { result[0] = selector.select(); } catch (IOException ioe) { throw runtime.newRuntimeError("Error with selector: " + ioe.getMessage()); } } public void wakeup() { selector.wakeup(); } }); } switch ( result[0] ) { case READ_WOULD_BLOCK_RESULT : return runtime.newSymbol("wait_readable"); // exception: false case WRITE_WOULD_BLOCK_RESULT : return runtime.newSymbol("wait_writable"); // exception: false case 0 : return Boolean.FALSE; default : //key should always be contained in selectedKeys() here, however there is a bug in //JRuby <= 9.1.2.0 that makes this not always the case, so we have to check return selector.selectedKeys().contains(key) ? Boolean.TRUE : Boolean.FALSE; } } catch (InterruptedException interrupt) { return Boolean.FALSE; } finally { // Note: I don't like ignoring these exceptions, but it's // unclear how likely they are to happen or what damage we // might do by ignoring them. Note that the pieces are separate // so that we can ensure one failing does not affect the others // running. // clean up the key in the selector try { if ( key != null ) key.cancel(); if ( selector != null ) selector.selectNow(); } catch (Exception e) { // ignore debugStackTrace(runtime, e); } // shut down and null out the selector try { if ( selector != null ) { runtime.getSelectorPool().put(selector); } } catch (Exception e) { // ignore debugStackTrace(runtime, e); } if (blocking) { // remove this thread as a blocker against the given IO io.removeBlockingThread(thread); // clear thread state from blocking call thread.afterBlockingCall(); } } } private static final int READ_WOULD_BLOCK_RESULT = Integer.MIN_VALUE + 1; private static final int WRITE_WOULD_BLOCK_RESULT = Integer.MIN_VALUE + 2; private static void readWouldBlock(final Ruby runtime, final boolean exception, final int[] result) { if ( exception ) throw newSSLErrorWaitReadable(runtime, "read would block"); result[0] = READ_WOULD_BLOCK_RESULT; } private static void writeWouldBlock(final Ruby runtime, final boolean exception, final int[] result) { if ( exception ) throw newSSLErrorWaitWritable(runtime, "write would block"); result[0] = WRITE_WOULD_BLOCK_RESULT; } private void doHandshake(final boolean blocking) throws IOException { doHandshake(blocking, true); } // might return :wait_readable | :wait_writable in case (true, false) private IRubyObject doHandshake(final boolean blocking, final boolean exception) throws IOException { while (true) { Object sel = waitSelect(SelectionKey.OP_READ | SelectionKey.OP_WRITE, blocking, exception); if ( sel instanceof IRubyObject ) return (IRubyObject) sel; // :wait_readable | :wait_writable // if not blocking, raise EAGAIN if ( ! blocking && sel != Boolean.TRUE ) { throw getRuntime().newErrnoEAGAINError("Resource temporarily unavailable"); } // otherwise, proceed as before switch (handshakeStatus) { case FINISHED: case NOT_HANDSHAKING: if ( initialHandshake ) finishInitialHandshake(); return null; // OK case NEED_TASK: doTasks(); break; case NEED_UNWRAP: if (readAndUnwrap(blocking) == -1 && handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED) { throw new SSLHandshakeException("Socket closed"); } // during initialHandshake, calling readAndUnwrap that results UNDERFLOW // does not mean writable. we explicitly wait for readable channel to avoid // busy loop. if (initialHandshake && status == SSLEngineResult.Status.BUFFER_UNDERFLOW) { sel = waitSelect(SelectionKey.OP_READ, blocking, exception); if ( sel instanceof IRubyObject ) return (IRubyObject) sel; // :wait_readable } break; case NEED_WRAP: if ( netData.hasRemaining() ) { while ( flushData(blocking) ) { /* loop */ } } netData.clear(); SSLEngineResult result = engine.wrap(dummy, netData); handshakeStatus = result.getHandshakeStatus(); netData.flip(); flushData(blocking); break; default: throw new IllegalStateException("Unknown handshaking status: " + handshakeStatus); } } } private void doTasks() { Runnable task; while ((task = engine.getDelegatedTask()) != null) { task.run(); } handshakeStatus = engine.getHandshakeStatus(); verifyResult = sslContext.getLastVerifyResult(); } private boolean flushData(boolean blocking) throws IOException { try { writeToChannel(netData, blocking); } catch (IOException ioe) { netData.position(netData.limit()); throw ioe; } if ( netData.hasRemaining() ) { return true; } return false; } private int writeToChannel(ByteBuffer buffer, boolean blocking) throws IOException { int totalWritten = 0; while ( buffer.hasRemaining() ) { totalWritten += socketChannelImpl().write(buffer); if ( ! blocking ) break; // don't continue attempting to read } return totalWritten; } private void finishInitialHandshake() { initialHandshake = false; } private void callRenegotiationCallback(final ThreadContext context) throws RaiseException { IRubyObject renegotiationCallback = sslContext.getInstanceVariable("@renegotiation_cb"); if(renegotiationCallback == null || renegotiationCallback.isNil()) { return; } else { // the return of the Proc is not important // Can throw ruby exception to "disallow" renegotiations renegotiationCallback.callMethod(context, "call", this); } } public int write(ByteBuffer src, boolean blocking) throws SSLException, IOException { if ( initialHandshake ) { throw new IOException("Writing not possible during handshake"); } SocketChannelImpl channel = socketChannelImpl(); final boolean blockingMode = channel.isBlocking(); if ( ! blocking ) channel.configureBlocking(false); try { if ( netData.hasRemaining() ) { flushData(blocking); } netData.clear(); final SSLEngineResult result = engine.wrap(src, netData); if ( result.getStatus() == SSLEngineResult.Status.CLOSED ) { throw getRuntime().newIOError("closed SSL engine"); } netData.flip(); flushData(blocking); return result.bytesConsumed(); } finally { if ( ! blocking ) channel.configureBlocking(blockingMode); } } public int read(final ByteBuffer dst, final boolean blocking) throws IOException { if ( initialHandshake ) return 0; if ( engine.isInboundDone() ) return -1; if ( ! peerAppData.hasRemaining() ) { int appBytesProduced = readAndUnwrap(blocking); if (appBytesProduced == -1 || appBytesProduced == 0) { return appBytesProduced; } } int limit = Math.min(peerAppData.remaining(), dst.remaining()); peerAppData.get(dst.array(), dst.arrayOffset(), limit); dst.position(dst.arrayOffset() + limit); return limit; } private int readAndUnwrap(final boolean blocking) throws IOException { final int bytesRead = socketChannelImpl().read(peerNetData); if ( bytesRead == -1 ) { if ( ! peerNetData.hasRemaining() || ( status == SSLEngineResult.Status.BUFFER_UNDERFLOW ) ) { closeInbound(); return -1; } // inbound channel has been already closed but closeInbound() must // be defered till the last engine.unwrap() call. // peerNetData could not be empty. } peerAppData.clear(); peerNetData.flip(); SSLEngineResult result; do { result = engine.unwrap(peerNetData, peerAppData); } while ( result.getStatus() == SSLEngineResult.Status.OK && result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP && result.bytesProduced() == 0 ); if ( result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.FINISHED ) { finishInitialHandshake(); } if ( peerAppData.position() == 0 && result.getStatus() == SSLEngineResult.Status.OK && peerNetData.hasRemaining() ) { result = engine.unwrap(peerNetData, peerAppData); } status = result.getStatus(); handshakeStatus = result.getHandshakeStatus(); if ( bytesRead == -1 && ! peerNetData.hasRemaining() ) { // now it's safe to call closeInbound(). closeInbound(); } if ( status == SSLEngineResult.Status.CLOSED ) { doShutdown(); return -1; } peerNetData.compact(); peerAppData.flip(); if ( ! initialHandshake && ( handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_TASK || handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP || handshakeStatus == SSLEngineResult.HandshakeStatus.FINISHED ) ) { doHandshake(blocking); } return peerAppData.remaining(); } private void closeInbound() { try { engine.closeInbound(); } catch (SSLException e) { debug(getRuntime(), "SSLSocket.closeInbound", e); // ignore any error on close. possibly an error like this; // Inbound closed before receiving peer's close_notify: possible truncation attack? } } private void doShutdown() throws IOException { if ( engine.isOutboundDone() ) return; netData.clear(); try { engine.wrap(dummy, netData); } catch (SSLException e) { debug(getRuntime(), "SSLSocket.doShutdown", e); return; } catch (RuntimeException e) { debugStackTrace(getRuntime(), e); return; } netData.flip(); flushData(true); } private IRubyObject sysreadImpl(final ThreadContext context, IRubyObject len, IRubyObject buff, final boolean blocking, final boolean exception) { final Ruby runtime = context.runtime; final int length = RubyNumeric.fix2int(len); final RubyString buffStr; if ( buff != null && ! buff.isNil() ) { buffStr = buff.asString(); } else { buffStr = RubyString.newEmptyString(runtime); // fine since we're setValue } if ( length == 0 ) { buffStr.clear(); return buffStr; } if ( length < 0 ) { throw runtime.newArgumentError("negative string size (or size too big)"); } try { // So we need to make sure to only block when there is no data left to process if ( engine == null || ! ( peerAppData.hasRemaining() || peerNetData.position() > 0 ) ) { final Object ex = waitSelect(SelectionKey.OP_READ, blocking, exception); if ( ex instanceof IRubyObject ) return (IRubyObject) ex; // :wait_readable } final ByteBuffer dst = ByteBuffer.allocate(length); int read = -1; // ensure >0 bytes read; sysread is blocking read. while ( read <= 0 ) { if ( engine == null ) { read = socketChannelImpl().read(dst); } else { read = read(dst, blocking); } if ( read == -1 ) { if ( exception ) throw runtime.newEOFError(); return runtime.getNil(); } if ( read == 0 && status == SSLEngineResult.Status.BUFFER_UNDERFLOW ) { // If we didn't get any data back because we only read in a partial TLS record, // instead of spinning until the rest comes in, call waitSelect to either block // until the rest is available, or throw a "read would block" error if we are in // non-blocking mode. final Object ex = waitSelect(SelectionKey.OP_READ, blocking, exception); if ( ex instanceof IRubyObject ) return (IRubyObject) ex; // :wait_readable } } final byte[] bytesRead = dst.array(); final int offset = dst.position() - read; buffStr.setValue(new ByteList(bytesRead, offset, read, false)); return buffStr; } catch (IOException ioe) { throw runtime.newIOError(ioe.getMessage()); } } @JRubyMethod public IRubyObject sysread(ThreadContext context, IRubyObject len) { return sysreadImpl(context, len, null, true, true); } @JRubyMethod public IRubyObject sysread(ThreadContext context, IRubyObject len, IRubyObject buff) { return sysreadImpl(context, len, buff, true, true); } @Deprecated // @JRubyMethod(rest = true, required = 1, optional = 1) public IRubyObject sysread(ThreadContext context, IRubyObject[] args) { switch ( args.length) { case 1 : return sysread(context, args[0]); case 2 : return sysread(context, args[0], args[1]); } Arity.checkArgumentCount(context.runtime, args.length, 1, 2); return null; // won't happen as checkArgumentCount raises } @JRubyMethod public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject len) { return sysreadImpl(context, len, null, false, true); } @JRubyMethod public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject len, IRubyObject arg) { if ( arg instanceof RubyHash ) { // exception: false // NOTE: on Ruby 2.3 this is expected to raise a TypeError (but not on 2.2) return sysreadImpl(context, len, null, false, getExceptionOpt(context, arg)); } return sysreadImpl(context, len, arg, false, true); // buffer arg } @JRubyMethod public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject len, IRubyObject buff, IRubyObject opts) { return sysreadImpl(context, len, buff, false, getExceptionOpt(context, opts)); } @Deprecated // @JRubyMethod(rest = true, required = 1, optional = 2) public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject[] args) { switch ( args.length) { case 1 : return sysread_nonblock(context, args[0]); case 2 : return sysread_nonblock(context, args[0], args[1]); case 3 : return sysread_nonblock(context, args[0], args[1], args[2]); } Arity.checkArgumentCount(context.runtime, args.length, 1, 3); return null; // won't happen as checkArgumentCount raises } private IRubyObject syswriteImpl(final ThreadContext context, final IRubyObject arg, final boolean blocking, final boolean exception) { final Ruby runtime = context.runtime; try { checkClosed(); final Object ex = waitSelect(SelectionKey.OP_WRITE, blocking, exception); if ( ex instanceof IRubyObject ) return (IRubyObject) ex; // :wait_writable ByteList bytes = arg.asString().getByteList(); ByteBuffer buff = ByteBuffer.wrap(bytes.getUnsafeBytes(), bytes.getBegin(), bytes.getRealSize()); final int written; if ( engine == null ) { written = writeToChannel(buff, blocking); } else { written = write(buff, blocking); } this.io.callMethod(context, "flush"); return runtime.newFixnum(written); } catch (IOException ioe) { throw runtime.newIOError(ioe.getMessage()); } } @JRubyMethod public IRubyObject syswrite(ThreadContext context, IRubyObject arg) { return syswriteImpl(context, arg, true, true); } @JRubyMethod public IRubyObject syswrite_nonblock(ThreadContext context, IRubyObject arg) { return syswriteImpl(context, arg, false, true); } @JRubyMethod public IRubyObject syswrite_nonblock(ThreadContext context, IRubyObject arg, IRubyObject opts) { return syswriteImpl(context, arg, false, getExceptionOpt(context, opts)); } private static boolean getExceptionOpt(final ThreadContext context, final IRubyObject opts) { if ( opts instanceof RubyHash ) { // exception: true final Ruby runtime = context.runtime; IRubyObject exc = ((RubyHash) opts).op_aref(context, runtime.newSymbol("exception")); return exc != runtime.getFalse(); } return true; } private void checkClosed() { if ( ! socketChannelImpl().isOpen() ) { throw getRuntime().newIOError("closed stream"); } } // do shutdown even if we have remaining data to be sent. // call this when you get an exception from client side. private void forceClose() { close(true); } private void close(boolean force) { if ( engine == null ) { // if ( force ) throw getRuntime().newEOFError(); return; } engine.closeOutbound(); if ( ! force && netData.hasRemaining() ) return; try { doShutdown(); } catch (IOException e) { // ignore? debug(getRuntime(), "SSLSocket.close doShutdown failed", e); } catch (NotYetConnectedException e) { debug(getRuntime(), "SSLSocket.close doShutdown failed", e); } } @JRubyMethod public IRubyObject sysclose(final ThreadContext context) { //if ( isClosed() ) return context.runtime.getNil(); if ( this.io.callMethod(context, "closed?").isTrue() ) { return context.runtime.getNil(); } // Ruby 2.3 // no need to try shutdown when it's a server close( sslContext.isProtocolForClient() ); if ( this.callMethod(context, "sync_close").isTrue() ) { return this.io.callMethod(context, "close"); } return context.runtime.getNil(); } @JRubyMethod public IRubyObject cert(final ThreadContext context) { final Ruby runtime = context.runtime; if ( engine == null ) return runtime.getNil(); try { Certificate[] cert = engine.getSession().getLocalCertificates(); if ( cert != null && cert.length > 0 ) { return X509Cert.wrap(context, cert[0]); } } catch (CertificateEncodingException e) { throw X509Cert.newCertificateError(runtime, e); } return runtime.getNil(); } // @Deprecated public final IRubyObject cert() { return cert(getRuntime().getCurrentContext()); } @JRubyMethod public IRubyObject peer_cert(final ThreadContext context) { final Ruby runtime = context.runtime; if ( engine == null ) return runtime.getNil(); try { Certificate[] cert = engine.getSession().getPeerCertificates(); if ( cert.length > 0 ) { return X509Cert.wrap(context, cert[0]); } } catch (CertificateEncodingException e) { throw X509Cert.newCertificateError(runtime, e); } catch (SSLPeerUnverifiedException e) { if (OpenSSL.isDebug(runtime)) { runtime.getWarnings().warning(String.format("%s: %s", e.getClass().getName(), e.getMessage())); } } return runtime.getNil(); } // @Deprecated public final IRubyObject peer_cert() { return peer_cert(getRuntime().getCurrentContext()); } @JRubyMethod public IRubyObject peer_cert_chain(final ThreadContext context) { final Ruby runtime = context.runtime; if ( engine == null ) return runtime.getNil(); try { javax.security.cert.Certificate[] certs = engine.getSession().getPeerCertificateChain(); IRubyObject[] cert_chain = new IRubyObject[ certs.length ]; for ( int i = 0; i < certs.length; i++ ) { cert_chain[i] = X509Cert.wrap(context, certs[i]); } return runtime.newArrayNoCopy(cert_chain); } catch (javax.security.cert.CertificateEncodingException e) { throw X509Cert.newCertificateError(getRuntime(), e); } catch (SSLPeerUnverifiedException e) { if (runtime.isVerbose() || OpenSSL.isDebug(runtime)) { runtime.getWarnings().warning(String.format("%s: %s", e.getClass().getName(), e.getMessage())); } } return runtime.getNil(); } // @Deprecated public final IRubyObject peer_cert_chain() { return peer_cert_chain(getRuntime().getCurrentContext()); } @JRubyMethod public IRubyObject cipher() { if ( engine == null ) return getRuntime().getNil(); return getRuntime().newString( engine.getSession().getCipherSuite() ); } @JRubyMethod public IRubyObject npn_protocol() { if ( engine == null ) return getRuntime().getNil(); // NOTE: maybe a time to use https://github.com/benmmurphy/ssl_npn warn(getRuntime().getCurrentContext(), "WARNING: SSLSocket#npn_protocol is not supported"); return getRuntime().getNil(); // throw new UnsupportedOperationException(); } @JRubyMethod public IRubyObject state() { warn(getRuntime().getCurrentContext(), "WARNING: unimplemented method called: SSLSocket#state"); return getRuntime().getNil(); } @JRubyMethod public IRubyObject pending() { warn(getRuntime().getCurrentContext(), "WARNING: unimplemented method called: SSLSocket#pending"); return getRuntime().getNil(); } private boolean reusableSSLEngine() { if ( engine != null ) { final String peerHost = engine.getPeerHost(); if ( peerHost != null && peerHost.length() > 0 ) { // NOT getSSLContext().createSSLEngine() - no hints for session reuse return true; } } return false; } @JRubyMethod(name = "session_reused?") public IRubyObject session_reused_p() { if ( reusableSSLEngine() ) { if ( ! engine.getEnableSessionCreation() ) { // if session creation is disabled we can be sure its to be re-used return getRuntime().getTrue(); } //return getRuntime().getFalse(); // NOTE: likely incorrect (we can not decide) } //warn(getRuntime().getCurrentContext(), "WARNING: SSLSocket#session_reused? is not supported"); return getRuntime().getNil(); // can not decide - probably not } // JSSE: SSL Sessions can be reused only if connecting to the same host at the same port final javax.net.ssl.SSLSession sslSession() { return engine == null ? null : engine.getSession(); } private transient SSLSession session; @JRubyMethod(name = "session") public IRubyObject session(final ThreadContext context) { if ( sslSession() == null ) return context.nil; return getSession(context.runtime); } private SSLSession getSession(final Ruby runtime) { if ( session == null ) { return session = new SSLSession(runtime).initializeImpl(this); } return session; } private transient SSLSession setSession = null; @JRubyMethod(name = "session=") public IRubyObject set_session(IRubyObject session) { // NOTE: we can not fully support this without the SSL provider internals // but we can assume setting a session= is meant as a forced session re-use if ( session instanceof SSLSession ) { setSession = (SSLSession) session; if ( engine != null ) copySessionSetupIfSet(); } //warn(context, "WARNING: SSLSocket#session= has not effect"); return getRuntime().getNil(); } private void copySessionSetupIfSet() { if ( setSession != null ) { if ( reusableSSLEngine() ) { engine.setEnableSessionCreation(false); final ThreadContext context = getRuntime().getCurrentContext(); if ( ! setSession.equals( getSession(context.runtime) ) ) { getSession(context.runtime).set_timeout(context, setSession.timeout(context)); } } } } @JRubyMethod public IRubyObject ssl_version() { if ( engine == null ) return getRuntime().getNil(); return getRuntime().newString( engine.getSession().getProtocol() ); } private transient SocketChannelImpl socketChannel; private SocketChannelImpl socketChannelImpl() { if ( socketChannel != null ) return socketChannel; final Channel channel = io.getChannel(); if ( channel instanceof SocketChannel ) { return socketChannel = new JavaSocketChannel((SocketChannel) channel); } // TODO JNR throw new IllegalStateException("unknow channel impl: " + channel + " of type " + channel.getClass().getName()); } private interface SocketChannelImpl { boolean isOpen() ; int read(ByteBuffer dst) throws IOException ; int write(ByteBuffer src) throws IOException ; int getRemotePort(); boolean isSelectable() ; // SelectableChannel boolean isBlocking() ; void configureBlocking(boolean block) throws IOException ; SelectionKey register(Selector selector, int ops) throws IOException ; //boolean selectionOpsReadable(final int readyOps); //boolean selectionOpsWritable(final int readyOps) ; } private static final class JavaSocketChannel implements SocketChannelImpl { JavaSocketChannel(final SocketChannel channel) { this.channel = channel; } private final SocketChannel channel; public boolean isOpen() { return channel.isOpen(); } public int read(ByteBuffer dst) throws IOException { return channel.read(dst); } public int write(ByteBuffer src) throws IOException { return channel.write(src); } public int getRemotePort() { return channel.socket().getPort(); } public boolean isSelectable() { return true; // return channel instanceof SelectableChannel; } public boolean isBlocking() { return channel.isBlocking(); } public void configureBlocking(boolean block) throws IOException { channel.configureBlocking(block); } public SelectionKey register(Selector selector, int ops) throws ClosedChannelException { return channel.register(selector, ops); } public boolean selectionOpsReadable(final int readyOps) { return (readyOps & SelectionKey.OP_READ) != 0; } public boolean selectionOpsWritable(final int readyOps) { return (readyOps & SelectionKey.OP_WRITE) != 0; } } private static boolean jnrChannel(final Channel channel) { return channel.getClass().getName().startsWith("jnr."); } }// SSLSocket