/***** BEGIN LICENSE BLOCK *****
* Version: CPL 1.0/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Common 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/cpl-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 CPL, 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 CPL, 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.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 java.util.Set;
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 javax.net.ssl.SSLSession;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyIO;
import org.jruby.RubyModule;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
import org.jruby.RubyObjectAdapter;
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.javasupport.JavaEmbedUtils;
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;
/**
* @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
*/
public class SSLSocket extends RubyObject {
private static final long serialVersionUID = -2276327900350542644L;
private static ObjectAllocator SSLSOCKET_ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new SSLSocket(runtime, klass);
}
};
private static RubyObjectAdapter api = JavaEmbedUtils.newObjectAdapter();
public static void createSSLSocket(Ruby runtime, RubyModule mSSL) {
RubyClass cSSLSocket = mSSL.defineClassUnder("SSLSocket",runtime.getObject(),SSLSOCKET_ALLOCATOR);
cSSLSocket.addReadWriteAttribute(runtime.getCurrentContext(), "io");
cSSLSocket.addReadWriteAttribute(runtime.getCurrentContext(), "context");
cSSLSocket.addReadWriteAttribute(runtime.getCurrentContext(), "sync_close");
cSSLSocket.addReadWriteAttribute(runtime.getCurrentContext(), "hostname");
cSSLSocket.defineAlias("to_io","io");
cSSLSocket.defineAnnotatedMethods(SSLSocket.class);
}
public SSLSocket(Ruby runtime, RubyClass type) {
super(runtime,type);
verifyResult = X509Utils.V_OK;
}
public static RaiseException newSSLError(Ruby runtime, String message) {
return Utils.newError(runtime, "OpenSSL::SSL::SSLError", message, false);
}
public static RaiseException newSSLErrorReadable(Ruby runtime, String message) {
return Utils.newError(runtime, "OpenSSL::SSL::SSLErrorReadable", message, false);
}
public static RaiseException newSSLErrorWritable(Ruby runtime, String message) {
return Utils.newError(runtime, "OpenSSL::SSL::SSLErrorWritable", message, false);
}
private org.jruby.ext.openssl.SSLContext rubyCtx;
private SSLEngine engine;
private RubyIO io = null;
private ByteBuffer peerAppData;
private ByteBuffer peerNetData;
private ByteBuffer netData;
private ByteBuffer dummy;
private boolean initialHandshake = false;
private SSLEngineResult.HandshakeStatus hsStatus;
private SSLEngineResult.Status status = null;
int verifyResult;
@JRubyMethod(name = "initialize", rest = true, frame = true)
public IRubyObject _initialize(IRubyObject[] args, Block unused) {
if (Arity.checkArgumentCount(getRuntime(), args, 1, 2) == 1) {
RubyClass sslContext = Utils.getClassFromPath(getRuntime(), "OpenSSL::SSL::SSLContext");
rubyCtx = (org.jruby.ext.openssl.SSLContext) api.callMethod(sslContext, "new");
} else {
rubyCtx = (org.jruby.ext.openssl.SSLContext) args[1];
}
Utils.checkKind(getRuntime(), args[0], "IO");
io = (RubyIO) args[0];
api.callMethod(this, "io=", io);
api.callMethod(this, "hostname=", getRuntime().newString(""));
// 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.
api.callMethod(io, "sync=", getRuntime().getTrue());
api.callMethod(this, "context=", rubyCtx);
api.callMethod(this, "sync_close=", getRuntime().getFalse());
rubyCtx.setup();
return api.callSuper(this, args);
}
private void ossl_ssl_setup() throws NoSuchAlgorithmException, KeyManagementException, IOException {
if(null == engine) {
Socket socket = getSocketChannel().socket();
// Server Name Indication (SNI) RFC 3546
// SNI support will not be attempted unless hostname is explicitly set by the caller
String peerHost = api.callMethod(this,"hostname").convertToString().toString();
int peerPort = socket.getPort();
engine = rubyCtx.createSSLEngine(peerHost, peerPort);
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);
}
}
@JRubyMethod
public IRubyObject connect(ThreadContext context) {
return connectCommon(context, true);
}
@JRubyMethod
public IRubyObject connect_nonblock(ThreadContext context) {
return connectCommon(context, false);
}
private IRubyObject connectCommon(ThreadContext context, boolean blocking) {
Ruby runtime = context.runtime;
if (!rubyCtx.isProtocolForClient()) {
throw newSSLError(runtime, "called a function you should not call");
}
try {
if (!initialHandshake) {
ossl_ssl_setup();
engine.setUseClientMode(true);
engine.beginHandshake();
hsStatus = engine.getHandshakeStatus();
initialHandshake = true;
}
doHandshake(blocking);
} catch(SSLHandshakeException e) {
// unlike server side, client should close outbound channel even if
// we have remaining data to be sent.
forceClose();
Throwable v = e;
while(v.getCause() != null && (v instanceof SSLHandshakeException)) {
v = v.getCause();
}
throw SSL.newSSLError(runtime, v);
} catch (NoSuchAlgorithmException ex) {
forceClose();
throw SSL.newSSLError(runtime, ex);
} catch (KeyManagementException ex) {
forceClose();
throw SSL.newSSLError(runtime, ex);
} catch (IOException ex) {
forceClose();
throw SSL.newSSLError(runtime, ex);
}
return this;
}
@JRubyMethod
public IRubyObject accept(ThreadContext context) {
return acceptCommon(context, true);
}
@JRubyMethod
public IRubyObject accept_nonblock(ThreadContext context) {
return acceptCommon(context, false);
}
public IRubyObject acceptCommon(ThreadContext context, boolean blocking) {
Ruby runtime = context.runtime;
if (!rubyCtx.isProtocolForServer()) {
throw newSSLError(runtime, "called a function you should not call");
}
try {
int vfy = 0;
if (!initialHandshake) {
ossl_ssl_setup();
engine.setUseClientMode(false);
if(!rubyCtx.isNil() && !rubyCtx.callMethod(context,"verify_mode").isNil()) {
vfy = RubyNumeric.fix2int(rubyCtx.callMethod(context,"verify_mode"));
if(vfy == 0) { //VERIFY_NONE
engine.setNeedClientAuth(false);
engine.setWantClientAuth(false);
}
if((vfy & 1) != 0) { //VERIFY_PEER
engine.setWantClientAuth(true);
}
if((vfy & 2) != 0) { //VERIFY_FAIL_IF_NO_PEER_CERT
engine.setNeedClientAuth(true);
}
}
engine.beginHandshake();
hsStatus = engine.getHandshakeStatus();
initialHandshake = true;
}
doHandshake(blocking);
} catch(SSLHandshakeException e) {
throw SSL.newSSLError(runtime, e);
} catch (NoSuchAlgorithmException ex) {
throw SSL.newSSLError(runtime, ex);
} catch (KeyManagementException ex) {
throw SSL.newSSLError(runtime, ex);
} catch (IOException ex) {
throw SSL.newSSLError(runtime, ex);
}
return this;
}
@JRubyMethod
public IRubyObject verify_result() {
if (engine == null) {
getRuntime().getWarnings().warn("SSL session is not started yet.");
return getRuntime().getNil();
}
return getRuntime().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 boolean waitSelect(final int operations, final boolean blocking) throws IOException {
if (!(io.getChannel() instanceof SelectableChannel)) {
return true;
}
final Ruby runtime = getRuntime();
RubyThread thread = runtime.getCurrentContext().getThread();
SelectableChannel selectable = (SelectableChannel)io.getChannel();
selectable.configureBlocking(false);
final Selector selector = runtime.getSelectorPool().get();
final SelectionKey key = selectable.register(selector, operations);
try {
io.addBlockingThread(thread);
final int[] result = new int[1];
thread.executeBlockingTask(new RubyThread.BlockingTask() {
public void run() throws InterruptedException {
try {
if (!blocking) {
result[0] = selector.selectNow();
if (result[0] == 0) {
if ((operations & SelectionKey.OP_READ) != 0 && (operations & SelectionKey.OP_WRITE) != 0) {
if (key.isReadable()) {
writeWouldBlock();
} else if (key.isWritable()) {
readWouldBlock();
} else { //neither, pick one
readWouldBlock();
}
} else if ((operations & SelectionKey.OP_READ) != 0) {
readWouldBlock();
} else if ((operations & SelectionKey.OP_WRITE) != 0) {
writeWouldBlock();
}
}
} else {
result[0] = selector.select();
}
} catch (IOException ioe) {
throw runtime.newRuntimeError("Error with selector: " + ioe.getMessage());
}
}
public void wakeup() {
selector.wakeup();
}
});
if (result[0] >= 1) {
Set<SelectionKey> keySet = selector.selectedKeys();
if (keySet.iterator().next() == key) {
return true;
}
}
return false;
} catch (InterruptedException ie) {
return 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
}
// shut down and null out the selector
try {
if (selector != null) {
runtime.getSelectorPool().put(selector);
}
} catch (Exception e) {
// ignore
}
// remove this thread as a blocker against the given IO
io.removeBlockingThread(thread);
// clear thread state from blocking call
thread.afterBlockingCall();
}
}
private void readWouldBlock() {
Ruby runtime = getRuntime();
throw newSSLErrorReadable(runtime, "read would block");
}
private void writeWouldBlock() {
Ruby runtime = getRuntime();
throw newSSLErrorWritable(runtime, "write would block");
}
private void doHandshake(boolean blocking) throws IOException {
while (true) {
SSLEngineResult res;
boolean ready = waitSelect(SelectionKey.OP_READ | SelectionKey.OP_WRITE, blocking);
// if not blocking, raise EAGAIN
if (!blocking && !ready) {
Ruby runtime = getRuntime();
throw runtime.is1_9() ?
runtime.newErrnoEAGAINWritableError("Resource temporarily unavailable") :
runtime.newErrnoEAGAINError("Resource temporarily unavailable");
}
// otherwise, proceed as before
switch (hsStatus) {
case FINISHED:
if (initialHandshake) {
finishInitialHandshake();
}
return;
case NEED_TASK:
doTasks();
break;
case NEED_UNWRAP:
if (readAndUnwrap(blocking) == -1 && hsStatus != 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) {
waitSelect(SelectionKey.OP_READ, blocking);
}
break;
case NEED_WRAP:
if (netData.hasRemaining()) {
while (flushData(blocking)) {
}
}
netData.clear();
res = engine.wrap(dummy, netData);
hsStatus = res.getHandshakeStatus();
netData.flip();
flushData(blocking);
break;
case NOT_HANDSHAKING:
// Opposite side could close while unwrapping. Handle this as same as FINISHED
return;
default:
throw new IllegalStateException("Unknown handshaking status: " + hsStatus);
}
}
}
private void doTasks() {
Runnable task;
while ((task = engine.getDelegatedTask()) != null) {
task.run();
}
hsStatus = engine.getHandshakeStatus();
verifyResult = rubyCtx.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 false;
} else {
return true;
}
}
private int writeToChannel(ByteBuffer buffer, boolean blocking) throws IOException {
int totalWritten = 0;
while (buffer.hasRemaining()) {
totalWritten += getSocketChannel().write(buffer);
if (!blocking) break; // don't continue attempting to read
}
return totalWritten;
}
private void finishInitialHandshake() {
initialHandshake = false;
}
public int write(ByteBuffer src, boolean blocking) throws SSLException, IOException {
if(initialHandshake) {
throw new IOException("Writing not possible during handshake");
}
SelectableChannel selectable = getSocketChannel();
boolean blockingMode = selectable.isBlocking();
if (!blocking) selectable.configureBlocking(false);
try {
if(netData.hasRemaining()) {
flushData(blocking);
}
netData.clear();
SSLEngineResult res = engine.wrap(src, netData);
netData.flip();
flushData(blocking);
return res.bytesConsumed();
} finally {
if (!blocking) selectable.configureBlocking(blockingMode);
}
}
public int read(ByteBuffer dst, 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(boolean blocking) throws IOException {
int bytesRead = getSocketChannel().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 res;
do {
res = engine.unwrap(peerNetData, peerAppData);
} while (res.getStatus() == SSLEngineResult.Status.OK &&
res.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP &&
res.bytesProduced() == 0);
if(res.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.FINISHED) {
finishInitialHandshake();
}
if(peerAppData.position() == 0 &&
res.getStatus() == SSLEngineResult.Status.OK &&
peerNetData.hasRemaining()) {
res = engine.unwrap(peerNetData, peerAppData);
}
status = res.getStatus();
hsStatus = res.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 && (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK ||
hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP ||
hsStatus == SSLEngineResult.HandshakeStatus.FINISHED)) {
doHandshake(blocking);
}
return peerAppData.remaining();
}
private void closeInbound() {
try {
engine.closeInbound();
} catch (SSLException ssle) {
// 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(Exception e1) {
return;
}
netData.flip();
flushData(true);
}
private IRubyObject do_sysread(ThreadContext context, IRubyObject[] args, boolean blocking) {
Ruby runtime = context.runtime;
int len = RubyNumeric.fix2int(args[0]);
RubyString str = null;
if (args.length == 2 && !args[1].isNil()) {
str = args[1].convertToString();
} else {
str = getRuntime().newString("");
}
if(len == 0) {
str.clear();
return str;
}
if (len < 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)) {
waitSelect(SelectionKey.OP_READ, blocking);
}
ByteBuffer dst = ByteBuffer.allocate(len);
int rr = -1;
// ensure >0 bytes read; sysread is blocking read.
while (rr <= 0) {
if (engine == null) {
rr = getSocketChannel().read(dst);
} else {
rr = read(dst, blocking);
}
if (rr == -1) {
throw getRuntime().newEOFError();
}
}
byte[] bss = new byte[rr];
dst.position(dst.position() - rr);
dst.get(bss);
str.setValue(new ByteList(bss));
return str;
} catch (IOException ioe) {
throw getRuntime().newIOError(ioe.getMessage());
}
}
@JRubyMethod(rest = true, required = 1, optional = 1)
public IRubyObject sysread(ThreadContext context, IRubyObject[] args) {
return do_sysread(context, args, true);
}
@JRubyMethod(rest = true, required = 1, optional = 1)
public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject[] args) {
return do_sysread(context, args, false);
}
private IRubyObject do_syswrite(ThreadContext context, IRubyObject arg, boolean blocking) {
Ruby runtime = context.runtime;
try {
checkClosed();
waitSelect(SelectionKey.OP_WRITE, blocking);
ByteList bls = arg.convertToString().getByteList();
ByteBuffer b1 = ByteBuffer.wrap(bls.getUnsafeBytes(), bls.getBegin(), bls.getRealSize());
int written;
if(engine == null) {
written = writeToChannel(b1, blocking);
} else {
written = write(b1, blocking);
}
((RubyIO)api.callMethod(this,"io")).flush();
return getRuntime().newFixnum(written);
} catch (IOException ioe) {
throw runtime.newIOError(ioe.getMessage());
}
}
@JRubyMethod
public IRubyObject syswrite(ThreadContext context, IRubyObject arg) {
return do_syswrite(context, arg, true);
}
@JRubyMethod
public IRubyObject syswrite_nonblock(ThreadContext context, IRubyObject arg) {
return do_syswrite(context, arg, false);
}
private void checkClosed() {
if (!getSocketChannel().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) throw getRuntime().newEOFError();
engine.closeOutbound();
if (!force && netData.hasRemaining()) {
return;
} else {
try {
doShutdown();
} catch (IOException ex) {
// ignore?
}
}
}
@JRubyMethod
public IRubyObject sysclose() {
// no need to try shutdown when it's a server
close(rubyCtx.isProtocolForClient());
ThreadContext tc = getRuntime().getCurrentContext();
if(callMethod(tc,"sync_close").isTrue()) {
callMethod(tc,"io").callMethod(tc,"close");
}
return getRuntime().getNil();
}
@JRubyMethod
public IRubyObject cert() {
if (engine == null) {
return getRuntime().getNil();
}
try {
Certificate[] cert = engine.getSession().getLocalCertificates();
if (cert != null && cert.length > 0) {
return X509Cert.wrap(getRuntime(), cert[0]);
}
} catch (CertificateEncodingException ex) {
throw X509Cert.newCertificateError(getRuntime(), ex);
}
return getRuntime().getNil();
}
@JRubyMethod
public IRubyObject peer_cert() {
if (engine == null) {
return getRuntime().getNil();
}
try {
Certificate[] cert = engine.getSession().getPeerCertificates();
if (cert.length > 0) {
return X509Cert.wrap(getRuntime(), cert[0]);
}
} catch (CertificateEncodingException ex) {
throw X509Cert.newCertificateError(getRuntime(), ex);
} catch (SSLPeerUnverifiedException ex) {
if (getRuntime().isVerbose()) {
getRuntime().getWarnings().warning(String.format("%s: %s", ex.getClass().getName(), ex.getMessage()));
}
}
return getRuntime().getNil();
}
@JRubyMethod
public IRubyObject peer_cert_chain() {
if (engine == null) {
return getRuntime().getNil();
}
try {
javax.security.cert.Certificate[] certs = engine.getSession().getPeerCertificateChain();
RubyArray arr = getRuntime().newArray(certs.length);
for (int i = 0; i < certs.length; i++) {
arr.add(X509Cert.wrap(getRuntime(), certs[i]));
}
return arr;
} catch (javax.security.cert.CertificateEncodingException e) {
throw X509Cert.newCertificateError(getRuntime(), e);
} catch (SSLPeerUnverifiedException ex) {
if (getRuntime().isVerbose()) {
getRuntime().getWarnings().warning(String.format("%s: %s", ex.getClass().getName(), ex.getMessage()));
}
}
return getRuntime().getNil();
}
@JRubyMethod
public IRubyObject cipher() {
return getRuntime().newString(engine.getSession().getCipherSuite());
}
@JRubyMethod
public IRubyObject state() {
System.err.println("WARNING: unimplemented method called: SSLSocket#state");
return getRuntime().getNil();
}
@JRubyMethod
public IRubyObject pending() {
System.err.println("WARNING: unimplemented method called: SSLSocket#pending");
return getRuntime().getNil();
}
@JRubyMethod
public IRubyObject session_reused_p() {
throw new UnsupportedOperationException();
}
@JRubyMethod
public synchronized IRubyObject session_set(IRubyObject aSession) {
throw new UnsupportedOperationException();
}
private SocketChannel getSocketChannel() {
return (SocketChannel) io.getChannel();
}
}// SSLSocket